mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-10 10:57:43 +01:00
Compare commits
418 Commits
v3.1-beta1
...
v3.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f689223b4 | ||
|
|
70ce7293ac | ||
|
|
94a0a3b568 | ||
|
|
69305f0509 | ||
|
|
24f48b11e6 | ||
|
|
ff3b48fa59 | ||
|
|
db3f478598 | ||
|
|
e20ac803f3 | ||
|
|
ea283365e7 | ||
|
|
8211830bd8 | ||
|
|
2a8e0f9404 | ||
|
|
c15cfc26f1 | ||
|
|
4f4e6938eb | ||
|
|
8545a547b9 | ||
|
|
3bb7184f28 | ||
|
|
dd71942a5e | ||
|
|
19fdd5e151 | ||
|
|
f537dc632e | ||
|
|
2221006970 | ||
|
|
5d29c5958b | ||
|
|
64dd46c7e4 | ||
|
|
8df382d976 | ||
|
|
69eb6b11d0 | ||
|
|
1f2d4fd2b3 | ||
|
|
21468fff25 | ||
|
|
4711b4d529 | ||
|
|
29d4859e02 | ||
|
|
4b81d86311 | ||
|
|
38963e7960 | ||
|
|
1584d51433 | ||
|
|
98571c62a6 | ||
|
|
69f525bfd3 | ||
|
|
2b31154834 | ||
|
|
b0948ea018 | ||
|
|
a50e4e3380 | ||
|
|
5564664b13 | ||
|
|
1ae5a2c808 | ||
|
|
0181a25d70 | ||
|
|
60ba4a9830 | ||
|
|
3802a78c9d | ||
|
|
0ca6d73614 | ||
|
|
aa77f8f0d2 | ||
|
|
381796e708 | ||
|
|
62fc7717c8 | ||
|
|
e19451bb4f | ||
|
|
85f588e8c9 | ||
|
|
ea644868a6 | ||
|
|
d08accaaf1 | ||
|
|
f49272cacb | ||
|
|
be8fef0228 | ||
|
|
b584f09223 | ||
|
|
d2968c95df | ||
|
|
7421e5f7d7 | ||
|
|
0b2a43cfcc | ||
|
|
50309d3ab3 | ||
|
|
dd0b16bff5 | ||
|
|
d5443adc74 | ||
|
|
9152ba72f1 | ||
|
|
076ca46ab4 | ||
|
|
02519b270e | ||
|
|
5aa7dedccb | ||
|
|
6383dfa854 | ||
|
|
5a4fb0323b | ||
|
|
e84a282aa6 | ||
|
|
f732493473 | ||
|
|
f66a265fcf | ||
|
|
f1472d218e | ||
|
|
d65c05aacd | ||
|
|
2b28ffa2f4 | ||
|
|
10ec31df3e | ||
|
|
184b1055dc | ||
|
|
eaec25e6c2 | ||
|
|
b63e29610e | ||
|
|
b0db5a8b0a | ||
|
|
d3e2241ff7 | ||
|
|
e90b9f6c19 | ||
|
|
4c1199e009 | ||
|
|
65471068b6 | ||
|
|
c6467a824b | ||
|
|
b1d1f3c6b2 | ||
|
|
574c2e2770 | ||
|
|
aec2d233c9 | ||
|
|
39418f2bbe | ||
|
|
ccda73494f | ||
|
|
443b4ccc57 | ||
|
|
511aedd5db | ||
|
|
2524290099 | ||
|
|
01e8017265 | ||
|
|
8338fc405f | ||
|
|
0a22b3990f | ||
|
|
662cafe416 | ||
|
|
ea961ba8f2 | ||
|
|
8c8774cd2f | ||
|
|
2fe02ddb1f | ||
|
|
e11e8a5d64 | ||
|
|
79bebf7c9b | ||
|
|
8d3b660ce0 | ||
|
|
9de53fe070 | ||
|
|
ecb9fc65b7 | ||
|
|
7b25d0379f | ||
|
|
05d4176d34 | ||
|
|
7b0dff88ae | ||
|
|
1c7604e0fe | ||
|
|
e18dc43aae | ||
|
|
caaad684a4 | ||
|
|
cdd51aee75 | ||
|
|
51851f6c99 | ||
|
|
ab98aa489c | ||
|
|
5829985ca8 | ||
|
|
2fa8e27f05 | ||
|
|
68f92dfd5d | ||
|
|
67aeb380e7 | ||
|
|
f7d91b7139 | ||
|
|
b6e157f393 | ||
|
|
2319fce092 | ||
|
|
a5f1707662 | ||
|
|
6cda55da06 | ||
|
|
c3f2fee633 | ||
|
|
1f575a2a47 | ||
|
|
13c4d13157 | ||
|
|
43fadab3bb | ||
|
|
82a0240d2e | ||
|
|
f2aa35d3d2 | ||
|
|
9c9fcaf42f | ||
|
|
146a51ceba | ||
|
|
b0350e9e96 | ||
|
|
35e346c4b9 | ||
|
|
1987647cc3 | ||
|
|
542534aeba | ||
|
|
908a2824ba | ||
|
|
cab9733b60 | ||
|
|
99e0dcec76 | ||
|
|
9dafb36c88 | ||
|
|
3d7d19b608 | ||
|
|
d650d10cb2 | ||
|
|
7fe45018e9 | ||
|
|
4c4cab87fb | ||
|
|
94c7f64baf | ||
|
|
f369b5f588 | ||
|
|
37065b7c50 | ||
|
|
0a7372460f | ||
|
|
063abc8ef7 | ||
|
|
fb4511d099 | ||
|
|
275560698f | ||
|
|
d4b6fe14c3 | ||
|
|
f1350a1022 | ||
|
|
344fb638fd | ||
|
|
373cc74a33 | ||
|
|
8e95ac42c2 | ||
|
|
ceb941df81 | ||
|
|
d275538116 | ||
|
|
fa38cdbc0d | ||
|
|
7569544b7b | ||
|
|
853a52f3ca | ||
|
|
39a0b15df4 | ||
|
|
a0db10838b | ||
|
|
f2f10dff92 | ||
|
|
7ba45b2887 | ||
|
|
c91eb8f406 | ||
|
|
57a78b3cad | ||
|
|
b755c7dab3 | ||
|
|
9ffd791ae4 | ||
|
|
8af12b22bb | ||
|
|
17ba0a97d5 | ||
|
|
4ae2b4e0b9 | ||
|
|
872691a138 | ||
|
|
3a54ecb522 | ||
|
|
42b590af77 | ||
|
|
b15ecf7649 | ||
|
|
df4f80e773 | ||
|
|
b8b485af4d | ||
|
|
892d6b55ec | ||
|
|
4a3bc8d365 | ||
|
|
e12da72615 | ||
|
|
f95e510060 | ||
|
|
82932ae7a5 | ||
|
|
14fc37a8b8 | ||
|
|
7b23856cc8 | ||
|
|
85f9690377 | ||
|
|
4723500c5f | ||
|
|
2db82a73a5 | ||
|
|
b00eeb86ea | ||
|
|
628e186846 | ||
|
|
cf4a55bc2f | ||
|
|
cab07c7c4b | ||
|
|
7735a539e9 | ||
|
|
68eb6fc3c1 | ||
|
|
fd785fc9a5 | ||
|
|
8d06908353 | ||
|
|
806706ca1d | ||
|
|
044e203eab | ||
|
|
fcc7207b67 | ||
|
|
8dbd3f332b | ||
|
|
f43ec7c05d | ||
|
|
ff9dde54e3 | ||
|
|
3699f16848 | ||
|
|
fee2ac2ebd | ||
|
|
57d3bfcfc9 | ||
|
|
b92e34556f | ||
|
|
b6ff55309e | ||
|
|
305d88ebda | ||
|
|
cdc73d4f56 | ||
|
|
0e50c964d5 | ||
|
|
863fb9aa47 | ||
|
|
298fb00a3e | ||
|
|
d1e8c06d36 | ||
|
|
8ed79d5973 | ||
|
|
85b10b59e4 | ||
|
|
9a53c22833 | ||
|
|
c981b5cba0 | ||
|
|
4ffa823ab8 | ||
|
|
001c7e4b18 | ||
|
|
402136dc8f | ||
|
|
59ee30f056 | ||
|
|
c795068a78 | ||
|
|
5ce080779b | ||
|
|
8d3b296eed | ||
|
|
cfdb985d00 | ||
|
|
af6f0db284 | ||
|
|
491eac184e | ||
|
|
414d33eb26 | ||
|
|
6dd6094088 | ||
|
|
2ec64a2ea2 | ||
|
|
5c34a75032 | ||
|
|
91f33d3289 | ||
|
|
c50dc1eb35 | ||
|
|
dc1331e736 | ||
|
|
afc866eee4 | ||
|
|
b6d93b7c5b | ||
|
|
5d6158dd64 | ||
|
|
e9549ab0bd | ||
|
|
779249ff81 | ||
|
|
66d206a710 | ||
|
|
bfc1cab6df | ||
|
|
5b0c79629e | ||
|
|
7922d3909a | ||
|
|
ee6e2e0af1 | ||
|
|
326a6be91c | ||
|
|
58095e1916 | ||
|
|
3dae077b4d | ||
|
|
7c14c0812b | ||
|
|
3a05eda63a | ||
|
|
d850b3ac7e | ||
|
|
08de6c32c9 | ||
|
|
91fe158c26 | ||
|
|
661b3c4bfb | ||
|
|
35eabc0353 | ||
|
|
ef5bbdb1e2 | ||
|
|
88fae2171d | ||
|
|
de698154cd | ||
|
|
1df05715c2 | ||
|
|
e5524da40e | ||
|
|
50d393e0f9 | ||
|
|
cd08836f3e | ||
|
|
45ac1cfd54 | ||
|
|
dda11ec69e | ||
|
|
7be6206d9d | ||
|
|
4d896573b1 | ||
|
|
988383648c | ||
|
|
d59847537d | ||
|
|
36859d89c8 | ||
|
|
cc50e22928 | ||
|
|
13414dcd25 | ||
|
|
ba8b593351 | ||
|
|
aebfccfd4b | ||
|
|
5a59f2352c | ||
|
|
5164b78da1 | ||
|
|
5561b46a59 | ||
|
|
26b2431cbf | ||
|
|
029605f926 | ||
|
|
0cd173f9df | ||
|
|
414810bdf5 | ||
|
|
f94c1e91ea | ||
|
|
b7129e1456 | ||
|
|
dc6decd404 | ||
|
|
40c6b172f7 | ||
|
|
7cb9cedfe1 | ||
|
|
b43980d660 | ||
|
|
09b612546b | ||
|
|
a99d14c13f | ||
|
|
68f322a03b | ||
|
|
97f0414ff3 | ||
|
|
d5f308d9c9 | ||
|
|
1377eda0ba | ||
|
|
70259b0d04 | ||
|
|
f1466d6da3 | ||
|
|
ca07a88674 | ||
|
|
83010e278c | ||
|
|
dcfd332cbf | ||
|
|
dc3040550d | ||
|
|
3b25db919a | ||
|
|
09f038f997 | ||
|
|
bbdd3804c7 | ||
|
|
a0b9ac7bcc | ||
|
|
8bb0cba949 | ||
|
|
870aa3a265 | ||
|
|
86ada33577 | ||
|
|
869808b3f9 | ||
|
|
57ccbf44b8 | ||
|
|
416caa8f50 | ||
|
|
1e42fecf66 | ||
|
|
c9b00891ed | ||
|
|
497eacbea3 | ||
|
|
f90c591c78 | ||
|
|
175498940e | ||
|
|
eded00cbb3 | ||
|
|
038d7e0fa6 | ||
|
|
b7c9ca720a | ||
|
|
7072f207c0 | ||
|
|
5f59f458f4 | ||
|
|
b6fe613329 | ||
|
|
cd128e557c | ||
|
|
30a5c70260 | ||
|
|
beca978af5 | ||
|
|
98a830a6a0 | ||
|
|
ed2231e34b | ||
|
|
55049bb303 | ||
|
|
c210c6937b | ||
|
|
d2767f39f0 | ||
|
|
1c9d39d3e6 | ||
|
|
f16c6d81cf | ||
|
|
e8d6281007 | ||
|
|
8299845615 | ||
|
|
9ae5865c2d | ||
|
|
c2d0cfdfc0 | ||
|
|
5dd252731e | ||
|
|
7b9436d2b9 | ||
|
|
6a369ac985 | ||
|
|
23d90823a3 | ||
|
|
4bfb6b476c | ||
|
|
0d60099588 | ||
|
|
9a45547cda | ||
|
|
a000ded350 | ||
|
|
424ac29131 | ||
|
|
b7b5a5788f | ||
|
|
9de179cba8 | ||
|
|
94069e76c9 | ||
|
|
df9d67b873 | ||
|
|
6f7fbf7686 | ||
|
|
f32e694499 | ||
|
|
e5900a3fe3 | ||
|
|
6e151b044d | ||
|
|
516bea6a0a | ||
|
|
496cabcc53 | ||
|
|
d051db5083 | ||
|
|
660fc23e15 | ||
|
|
a5a480133f | ||
|
|
68b544c676 | ||
|
|
a8c958ece2 | ||
|
|
f77f7ca0ec | ||
|
|
6b21c8453f | ||
|
|
fa8a8abc98 | ||
|
|
80048bfa2b | ||
|
|
641a9bc6c5 | ||
|
|
0edf9b17f6 | ||
|
|
98cc36c458 | ||
|
|
f3beabba69 | ||
|
|
467fa5a847 | ||
|
|
50f283cf28 | ||
|
|
f49d7008a0 | ||
|
|
1fed564c47 | ||
|
|
bb99c3e6f9 | ||
|
|
8820cac792 | ||
|
|
ada911c20b | ||
|
|
17e01644f5 | ||
|
|
9458521f3e | ||
|
|
8aa73c5900 | ||
|
|
500f213c6b | ||
|
|
cede27b5fe | ||
|
|
c0ca1eaf90 | ||
|
|
b29a5511df | ||
|
|
49e77841e0 | ||
|
|
daf6c8e327 | ||
|
|
9f8068e8d1 | ||
|
|
0b705553a5 | ||
|
|
a799094227 | ||
|
|
d529c1b5b3 | ||
|
|
834f68e6e4 | ||
|
|
83b2102705 | ||
|
|
2f064cdfd1 | ||
|
|
6c28182dd3 | ||
|
|
3cb8c5db28 | ||
|
|
251abdb4dd | ||
|
|
726e4df54b | ||
|
|
bd32a6ac8e | ||
|
|
27d7400c36 | ||
|
|
53e52aeaa8 | ||
|
|
ae6ed97a80 | ||
|
|
34f24de3e4 | ||
|
|
f93d6813a9 | ||
|
|
3ad773beb3 | ||
|
|
be91235858 | ||
|
|
95fc0bbc94 | ||
|
|
9dad7e4daf | ||
|
|
d08ed9fe5f | ||
|
|
82210cc116 | ||
|
|
94d3e76517 | ||
|
|
3f72492a59 | ||
|
|
c0653da736 | ||
|
|
f3d8f1b1fb | ||
|
|
d2391b9c63 | ||
|
|
f8e44c09eb | ||
|
|
2a00519b93 | ||
|
|
3292a2aecc | ||
|
|
b7aa44837f | ||
|
|
17fd6e692e | ||
|
|
2ce8ef5704 | ||
|
|
7b7afd3e7b | ||
|
|
9c2514fce4 | ||
|
|
e04402ed57 | ||
|
|
3eda8d8482 | ||
|
|
79f2f03fb2 | ||
|
|
f7d0db9cd2 | ||
|
|
fab1d3651b | ||
|
|
e5d7578663 | ||
|
|
830cf4b31f | ||
|
|
b07e88869a | ||
|
|
94bd27bcf5 |
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -13,11 +13,8 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
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: v3.0.9
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.1.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.9
|
||||
placeholder: v3.1.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ yarn-error.log*
|
||||
!/netbox/project-static/docs/.info
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/local/*
|
||||
/netbox/reports/*
|
||||
!/netbox/reports/__init__.py
|
||||
/netbox/scripts/*
|
||||
|
||||
@@ -76,14 +76,10 @@ free to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||
|
||||
* Due to a large backlog of feature requests, we are not currently accepting
|
||||
any proposals which substantially extend NetBox's functionality beyond its
|
||||
current feature set. This includes the introduction of any new views or models
|
||||
which have not already been proposed in an existing feature request.
|
||||
|
||||
* Before filing a new feature request, consider raising your idea on the
|
||||
mailing list first. Feedback you receive there will help validate and shape the
|
||||
proposed feature before filing a formal issue.
|
||||
* Before filing a new feature request, consider raising your idea in a
|
||||
[GitHub discussion](https://github.com/netbox-community/netbox/discussions)
|
||||
first. Feedback you receive there will help validate and shape the proposed
|
||||
feature before filing a formal issue.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||
describe the functionality and data model(s) being proposed. The more effort
|
||||
|
||||
37
README.md
37
README.md
@@ -5,11 +5,46 @@
|
||||

|
||||
|
||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
||||
network automation. Initially conceived by the network engineering team at
|
||||
network automation, used by thousands of organizations around the world.
|
||||
Initially conceived by the network engineering team at
|
||||
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
|
||||
to address the needs of network and infrastructure engineers. It is intended to
|
||||
function as a domain-specific source of truth for network operations.
|
||||
|
||||
Myriad infrastructure components can be modeled in NetBox, including:
|
||||
|
||||
* Hierarchical regions, site groups, sites, and locations
|
||||
* Racks, devices, and device components
|
||||
* Cables and wireless connections
|
||||
* Power distribution
|
||||
* Data circuits and providers
|
||||
* Virtual machines and clusters
|
||||
* IP prefixes, ranges, and addresses
|
||||
* VRFs and route targets
|
||||
* FHRP groups (VRRP, HSRP, etc.)
|
||||
* AS numbers
|
||||
* VLANs and scoped VLAN groups
|
||||
* Organizational tenants and contacts
|
||||
|
||||
In addition to its extensive built-in models and functionality, NetBox can be
|
||||
customized and extended through the use of:
|
||||
|
||||
* Custom fields
|
||||
* Custom links
|
||||
* Configuration contexts
|
||||
* Custom model validation rules
|
||||
* Reports
|
||||
* Custom scripts
|
||||
* Export templates
|
||||
* Conditional webhooks
|
||||
* Plugins
|
||||
* Single sign-on (SSO) authentication
|
||||
* NAPALM integration
|
||||
* Detailed change logging
|
||||
|
||||
NetBox also features a complete REST API as well as a GraphQL API for easily
|
||||
integrating with other tools and systems.
|
||||
|
||||
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).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://github.com/django/django
|
||||
Django
|
||||
Django<4.0
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
@@ -98,13 +98,9 @@ psycopg2-binary
|
||||
# https://github.com/yaml/pyyaml
|
||||
PyYAML
|
||||
|
||||
# In-memory key/value store used for caching and queuing
|
||||
# https://github.com/andymccurdy/redis-py
|
||||
redis
|
||||
|
||||
# Social authentication framework
|
||||
# https://github.com/python-social-auth/social-core
|
||||
social-auth-core[all]
|
||||
social-auth-core
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django
|
||||
|
||||
0
contrib/netbox-housekeeping.sh
Normal file → Executable file
0
contrib/netbox-housekeeping.sh
Normal file → Executable file
@@ -1,5 +1,22 @@
|
||||
{!models/extras/webhook.md!}
|
||||
|
||||
## Conditional Webhooks
|
||||
|
||||
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
|
||||
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -31,6 +31,41 @@ This defines custom content to be displayed on the login page above the login fo
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
|
||||
changes in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"name": {
|
||||
"min_length": 5,
|
||||
"max_length": 30
|
||||
}
|
||||
},
|
||||
"my_plugin.validators.Validator1"
|
||||
],
|
||||
"dim.device": [
|
||||
"my_plugin.validators.Validator1"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
Default: False
|
||||
@@ -39,6 +74,14 @@ By default, NetBox will permit users to create duplicate prefixes and IP address
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## MAINTENANCE_MODE
|
||||
|
||||
Default: False
|
||||
|
||||
@@ -25,18 +25,6 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
|
||||
changes in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
||||
Default: False
|
||||
@@ -61,22 +49,6 @@ CORS_ORIGIN_WHITELIST = [
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
Validator1,
|
||||
Validator2,
|
||||
Validator3
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DEBUG
|
||||
|
||||
Default: False
|
||||
@@ -168,14 +140,6 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
|
||||
@@ -27,3 +27,13 @@ Device components represent discrete objects within a device which are used to t
|
||||
---
|
||||
|
||||
{!models/dcim/cable.md!}
|
||||
|
||||
In the example below, three individual cables comprise a path between devices A and D:
|
||||
|
||||

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -77,6 +77,10 @@ This is the human-friendly names of your script. If omitted, the class name will
|
||||
|
||||
A human-friendly description of what your script does.
|
||||
|
||||
### `field_order`
|
||||
|
||||
By default, script variables will be ordered in the form as they are defined in the script. `field_order` may be defined as an iterable of field names to determine the order in which variables are rendered. Any fields not included in this iterable be listed last.
|
||||
|
||||
### `commit_default`
|
||||
|
||||
The checkbox to commit database changes when executing a script is checked by default. Set `commit_default` to False under the script's Meta class to leave this option unchecked by default.
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
# Custom Validation
|
||||
|
||||
NetBox validates every object prior to it being written to the database to ensure data integrity. This validation includes things like checking for proper formatting and that references to related objects are valid. However, you may wish to supplement this validation with some rules of your own. For example, perhaps you require that every site's name conforms to a specific pattern. This can be done using NetBox's `CustomValidator` class.
|
||||
NetBox validates every object prior to it being written to the database to ensure data integrity. This validation includes things like checking for proper formatting and that references to related objects are valid. However, you may wish to supplement this validation with some rules of your own. For example, perhaps you require that every site's name conforms to a specific pattern. This can be done using custom validation rules.
|
||||
|
||||
## CustomValidator
|
||||
## Custom Validation Rules
|
||||
|
||||
### Validation Rules
|
||||
Custom validation rules are expressed as a mapping of model attributes to a set of rules to which that attribute must conform. For example:
|
||||
|
||||
A custom validator can be instantiated by passing a mapping of attributes to a set of rules to which that attribute must conform. For example:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
CustomValidator({
|
||||
'name': {
|
||||
'min_length': 5,
|
||||
'max_length': 30,
|
||||
}
|
||||
})
|
||||
```json
|
||||
{
|
||||
"name": {
|
||||
"min_length": 5,
|
||||
"max_length": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation.
|
||||
@@ -38,12 +34,13 @@ The `min` and `max` types should be defined for numeric values, whereas `min_len
|
||||
|
||||
### Custom Validation Logic
|
||||
|
||||
There may be instances where the provided validation types are insufficient. The `CustomValidator` class can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected.
|
||||
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected.
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
class MyValidator(CustomValidator):
|
||||
|
||||
def validate(self, instance):
|
||||
if instance.status == 'active' and not instance.description:
|
||||
self.fail("Active sites must have a description set!", field='status')
|
||||
@@ -53,34 +50,69 @@ The `fail()` method may optionally specify a field with which to associate the s
|
||||
|
||||
## Assigning Custom Validators
|
||||
|
||||
Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/optional-settings.md#custom_validators) configuration parameter, as such:
|
||||
Custom validators are associated with specific NetBox models under the [CUSTOM_VALIDATORS](../configuration/dynamic-settings.md#custom_validators) configuration parameter. There are three manners by which custom validation rules can be defined:
|
||||
|
||||
1. Plain JSON mapping (no custom logic)
|
||||
2. Dotted path to a custom validator class
|
||||
3. Direct reference to a custom validator class
|
||||
|
||||
### Plain Data
|
||||
|
||||
For cases where custom logic is not needed, it is sufficient to pass validation rules as plain JSON-compatible objects. This approach typically affords the most portability for your configuration. For instance:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"name": {
|
||||
"min_length": 5,
|
||||
"max_length": 30,
|
||||
}
|
||||
}
|
||||
],
|
||||
"dcim.device": [
|
||||
{
|
||||
"platform": {
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Dotted Path
|
||||
|
||||
In instances where a custom validator class is needed, it can be referenced by its Python path (relative to NetBox's working directory):
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
'my_validators.Validator1',
|
||||
'my_validators.Validator2',
|
||||
),
|
||||
'dcim.device': (
|
||||
'my_validators.Validator3',
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Class Reference
|
||||
|
||||
This approach requires each class being instantiated to be imported directly within the Python configuration file.
|
||||
|
||||
```python
|
||||
from my_validators import Validator1, Validator2, Validator3
|
||||
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
Validator1,
|
||||
Validator2,
|
||||
Validator3
|
||||
),
|
||||
'dcim.device': (
|
||||
Validator3,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Even if defining only a single validator, it must be passed as an iterable.
|
||||
|
||||
When it is not necessary to define a custom `validate()` method, you may opt to pass a `CustomValidator` instance directly:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
CustomValidator({
|
||||
'name': {
|
||||
'min_length': 5,
|
||||
'max_length': 30,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,9 +6,9 @@ Models within each app are stored in either `models.py` or within a submodule un
|
||||
|
||||
Each model should define, at a minimum:
|
||||
|
||||
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
|
||||
* A `__str__()` method returning a user-friendly string representation of the instance
|
||||
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
|
||||
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
|
||||
|
||||
## 2. Define field choices
|
||||
|
||||
@@ -16,9 +16,9 @@ If the model has one or more fields with static choices, define those choices in
|
||||
|
||||
## 3. Generate database migrations
|
||||
|
||||
Once your model definition is complete, generate database migrations by running `manage.py -n $NAME --no-header`. Always specify a short unique name when generating migrations.
|
||||
Once your model definition is complete, generate database migrations by running `manage.py makemigrations -n $NAME --no-header`. Always specify a short unique name when generating migrations.
|
||||
|
||||
!!! info
|
||||
!!! info "Configuration Required"
|
||||
Set `DEVELOPER = True` in your NetBox configuration to enable the creation of new migrations.
|
||||
|
||||
## 4. Add all standard views
|
||||
@@ -37,25 +37,32 @@ Most models will need view classes created in `views.py` to serve the following
|
||||
|
||||
Add the relevant URL path for each view created in the previous step to `urls.py`.
|
||||
|
||||
## 6. Create the FilterSet
|
||||
## 6. Add relevant forms
|
||||
|
||||
Depending on the type of model being added, you may need to define several types of form classes. These include:
|
||||
|
||||
* A base model form (for creating/editing individual objects)
|
||||
* A bulk edit form
|
||||
* A bulk import form (for CSV-based import)
|
||||
* A filterset form (for filtering the object list view)
|
||||
|
||||
## 7. Create the FilterSet
|
||||
|
||||
Each model should have a corresponding FilterSet class defined. This is used to filter UI and API queries. Subclass the appropriate class from `netbox.filtersets` that matches the model's parent class.
|
||||
|
||||
Every model FilterSet should define a `q` filter to support general search queries.
|
||||
|
||||
## 7. Create the table
|
||||
## 8. Create the table class
|
||||
|
||||
Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns.
|
||||
|
||||
## 8. Create the object template
|
||||
## 9. Create the object template
|
||||
|
||||
Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`.
|
||||
|
||||
## 9. Add the model to the navigation menu
|
||||
## 10. Add the model to the navigation menu
|
||||
|
||||
For NetBox releases prior to v3.0, add the relevant link(s) to the navigation menu template. For later releases, add the relevant items in `netbox/netbox/navigation_menu.py`.
|
||||
Add the relevant navigation menu items in `netbox/netbox/navigation_menu.py`.
|
||||
|
||||
## 10. REST API components
|
||||
## 11. REST API components
|
||||
|
||||
Create the following for each model:
|
||||
|
||||
@@ -64,13 +71,13 @@ Create the following for each model:
|
||||
* API view in `api/views.py`
|
||||
* Endpoint route in `api/urls.py`
|
||||
|
||||
## 11. GraphQL API components (v3.0+)
|
||||
## 12. GraphQL API components
|
||||
|
||||
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
||||
|
||||
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
|
||||
|
||||
## 12. Add tests
|
||||
## 13. Add tests
|
||||
|
||||
Add tests for the following:
|
||||
|
||||
@@ -78,7 +85,7 @@ Add tests for the following:
|
||||
* API views
|
||||
* Filter sets
|
||||
|
||||
## 13. Documentation
|
||||
## 14. Documentation
|
||||
|
||||
Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate.
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ Below is a list of tasks to consider when adding a new field to a core model.
|
||||
|
||||
## 1. Generate and run database migrations
|
||||
|
||||
Django migrations are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
|
||||
[Django migrations](https://docs.djangoproject.com/en/stable/topics/migrations/) are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
|
||||
|
||||
```
|
||||
./manage.py makemigrations <app> -n <name>
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
Where possible, try to merge related changes into a single migration. For example, if three new fields are being added to different models within an app, these can be expressed in the same migration. You can merge a new migration with an existing one by combining their `operations` lists.
|
||||
Where possible, try to merge related changes into a single migration. For example, if three new fields are being added to different models within an app, these can be expressed in a single migration. You can merge a newly generated migration with an existing one by combining their `operations` lists.
|
||||
|
||||
!!! note
|
||||
!!! warning "Do not alter existing migrations"
|
||||
Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered (other than for the purpose of correcting a bug).
|
||||
|
||||
## 2. Add validation logic to `clean()`
|
||||
@@ -24,7 +24,6 @@ If the new field introduces additional validation requirements (beyond what's in
|
||||
class Foo(models.Model):
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
# Custom validation goes here
|
||||
@@ -40,9 +39,9 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
|
||||
|
||||
## 5. Add field to forms
|
||||
## 5. Add fields to forms
|
||||
|
||||
Extend any forms to include the new field as appropriate. Common forms include:
|
||||
Extend any forms to include the new field(s) as appropriate. These are found under the `forms/` directory within each app. Common forms include:
|
||||
|
||||
* **Credit/edit** - Manipulating a single object
|
||||
* **Bulk edit** - Performing a change on many objects at once
|
||||
@@ -51,11 +50,11 @@ Extend any forms to include the new field as appropriate. Common forms include:
|
||||
|
||||
## 6. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to query it in the FilterSet's `search()` method.
|
||||
|
||||
## 7. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column.
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column. Also add the field name to `default_columns` if the column should be present in the table by default.
|
||||
|
||||
## 8. Update the UI templates
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ The NetBox project utilizes three persistent git branches to track work:
|
||||
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch, which receives merged only from the `develop` branch.
|
||||
|
||||
For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0).
|
||||
|
||||
### Enable Pre-Commit Hooks
|
||||
|
||||
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
|
||||
@@ -46,7 +48,7 @@ $ ln -s ../../scripts/git-hooks/pre-commit
|
||||
|
||||
### Create a Python Virtual Environment
|
||||
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) is like a container for a set of Python packages. They allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
|
||||
Create a virtual environment using the `venv` Python module:
|
||||
|
||||
@@ -57,8 +59,8 @@ $ python3 -m venv ~/.venv/netbox
|
||||
|
||||
This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`.
|
||||
|
||||
!!! info
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created wherever you please.
|
||||
!!! info "Where to Create Your Virtual Environments"
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please.
|
||||
|
||||
Once created, activate the virtual environment:
|
||||
|
||||
@@ -94,7 +96,7 @@ Within the `netbox/netbox/` directory, copy `configuration.example.py` to `confi
|
||||
|
||||
### Start the Development Server
|
||||
|
||||
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. NetBox extends this slightly to automatically import models and other utilities. Run the NetBox development server with the `nbshell` management command:
|
||||
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py runserver
|
||||
@@ -109,23 +111,38 @@ Quit the server with CONTROL-C.
|
||||
|
||||
This ensures that your development environment is now complete and operational. Any changes you make to the code base will be automatically adapted by the development server.
|
||||
|
||||
!!! info "IDE Integration"
|
||||
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||
|
||||
## Populating Demo Data
|
||||
|
||||
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
|
||||
|
||||
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
|
||||
|
||||
## Running Tests
|
||||
|
||||
Throughout the course of development, it's a good idea to occasionally run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command:
|
||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `/netbox/` directory, not the root directory of the repository.
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py test
|
||||
$ python manage.py test
|
||||
```
|
||||
|
||||
In cases where you haven't made any changes to the database (which is most of the time), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py test --keepdb
|
||||
$ python manage.py test --keepdb
|
||||
```
|
||||
|
||||
You can also limit the command to running only a specific subset of tests. For example, to run only IPAM and DCIM view tests:
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test dcim.tests.test_views ipam.tests.test_views
|
||||
```
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to reference it.
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
|
||||
|
||||
```no-highlight
|
||||
$ git commit -m "Closes #1234: Add IPv5 support"
|
||||
@@ -136,5 +153,5 @@ Once your fork has the new commit, submit a [pull request](https://github.com/ne
|
||||
|
||||
Once submitted, a maintainer will review your pull request and either merge it or request changes. If changes are needed, you can make them via new commits to your fork: The pull request will update automatically.
|
||||
|
||||
!!! note
|
||||
Remember, pull requests are entertained only for **accepted** issues. If an issue you want to work on hasn't been approved by a maintainer yet, it's best to avoid risking your time and effort on a change that might not be accepted.
|
||||
!!! note "Remember to Open an Issue First"
|
||||
Remember, pull requests are permitted only for **accepted** issues. If an issue you want to work on hasn't been approved by a maintainer yet, it's best to avoid risking your time and effort on a change that might not be accepted. (The one exception to this is trivial changes to the documentation or other non-critical resources.)
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
# NetBox Development
|
||||
|
||||
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Each pull request must be preceded by an **approved** issue. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||
|
||||
## Communication
|
||||
|
||||
There are several official forums for communication among the developers and community members:
|
||||
|
||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
|
||||
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
|
||||
|
||||
## Governance
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions (in other words, avoid scope creep).
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base.
|
||||
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base. Only pull requests representing new releases should be merged into `master`.
|
||||
|
||||
NetBox components are arranged into functional subsections called _apps_ (a carryover from Django vernacular). Each app holds the models, views, and templates relevant to a particular function:
|
||||
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
|
||||
|
||||
* `circuits`: Communications circuits and providers (not to be confused with power circuits)
|
||||
* `dcim`: Datacenter infrastructure management (sites, racks, and devices)
|
||||
@@ -29,3 +29,6 @@ NetBox components are arranged into functional subsections called _apps_ (a carr
|
||||
* `users`: Authentication and user preferences
|
||||
* `utilities`: Resources which are not user-facing (extendable classes, etc.)
|
||||
* `virtualization`: Virtual machines and clusters
|
||||
* `wireless`: Wireless links and LANs
|
||||
|
||||
All core functionality is stored within the `netbox/` subdirectory. HTML templates are stored in a common `templates/` directory, with model- and view-specific templates arranged by app. Documentation is kept in the `docs/` root directory.
|
||||
|
||||
@@ -17,12 +17,12 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* Nesting - These models can be nested recursively to create a hierarchy
|
||||
|
||||
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
||||
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
||||
| ------------------ | ---------------- | ---------------- |------------------| ---------------- | ---------------- | ---------------- | ---------------- |
|
||||
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
||||
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
|
||||
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
||||
| Component Template | :material-check: | :material-check: | | | | | |
|
||||
|
||||
## Models Index
|
||||
|
||||
@@ -41,15 +41,21 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [dcim.Site](../models/dcim/site.md)
|
||||
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
|
||||
* [ipam.Aggregate](../models/ipam/aggregate.md)
|
||||
* [ipam.ASN](../models/ipam/asn.md)
|
||||
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
|
||||
* [ipam.IPAddress](../models/ipam/ipaddress.md)
|
||||
* [ipam.IPRange](../models/ipam/iprange.md)
|
||||
* [ipam.Prefix](../models/ipam/prefix.md)
|
||||
* [ipam.RouteTarget](../models/ipam/routetarget.md)
|
||||
* [ipam.Service](../models/ipam/service.md)
|
||||
* [ipam.VLAN](../models/ipam/vlan.md)
|
||||
* [ipam.VRF](../models/ipam/vrf.md)
|
||||
* [tenancy.Contact](../models/tenancy/contact.md)
|
||||
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
||||
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
||||
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
||||
* [wireless.WirelessLAN](../models/wireless/wirelesslan.md)
|
||||
* [wireless.WirelessLink](../models/wireless/wirelesslink.md)
|
||||
|
||||
### Organizational Models
|
||||
|
||||
@@ -61,6 +67,7 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [ipam.RIR](../models/ipam/rir.md)
|
||||
* [ipam.Role](../models/ipam/role.md)
|
||||
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
|
||||
* [tenancy.ContactRole](../models/tenancy/contactrole.md)
|
||||
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
|
||||
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
|
||||
|
||||
@@ -69,7 +76,9 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
||||
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
|
||||
* [dcim.Region](../models/dcim/region.md)
|
||||
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
|
||||
* [tenancy.ContactGroup](../models/tenancy/contactgroup.md)
|
||||
* [tenancy.TenantGroup](../models/tenancy/tenantgroup.md)
|
||||
* [wireless.WirelessLANGroup](../models/wireless/wirelesslangroup.md)
|
||||
|
||||
### Component Models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh` for details.
|
||||
|
||||
## PEP 8 Exceptions
|
||||
|
||||
@@ -30,7 +30,7 @@ pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
## Introducing New Dependencies
|
||||
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
|
||||
|
||||
If there's a strong case for introducing a new dependency, it must meet the following criteria:
|
||||
|
||||
@@ -43,7 +43,7 @@ When adding a new dependency, a short description of the package and the URL of
|
||||
|
||||
## General Guidance
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
|
||||
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically offers several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
|
||||
|
||||
@@ -67,4 +67,4 @@ Authorization: Token $TOKEN
|
||||
|
||||
## Disabling the GraphQL API
|
||||
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/optional-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/dynamic-settings.md#graphql_enabled) configuration parameter to False and restarting NetBox.
|
||||
|
||||
@@ -50,7 +50,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
| Application | Django/Python |
|
||||
| Database | PostgreSQL 10+ |
|
||||
| Task queuing | Redis/django-rq |
|
||||
| Live device access | NAPALM |
|
||||
| Live device access | NAPALM (optional) |
|
||||
|
||||
## Supported Python Versions
|
||||
|
||||
@@ -58,4 +58,6 @@ NetBox supports Python 3.7, 3.8, and 3.9 environments currently. (Support for Py
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [installation guide](installation/index.md) for help getting NetBox up and running quickly.
|
||||
Minor NetBox releases (e.g. v3.1) are published three times a year; in April, August, and December. These typically introduce major new features and may contain breaking API changes. Patch releases are published roughly every one to two weeks to resolve bugs and fulfill minor feature requests. These are backward-compatible with previous releases unless otherwise noted. The NetBox maintainers strongly recommend running the latest stable release whenever possible.
|
||||
|
||||
Please see the [official installation guide](installation/index.md) for detailed instructions on obtaining and installing NetBox.
|
||||
|
||||
@@ -267,7 +267,7 @@ NetBox includes a `housekeeping` management command that handles some recurring
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
@@ -152,7 +152,7 @@ LOGGING = {
|
||||
'netbox_auth_log': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': '/opt/netbox/logs/django-ldap-debug.log',
|
||||
'filename': '/opt/netbox/local/logs/django-ldap-debug.log',
|
||||
'maxBytes': 1024 * 500,
|
||||
'backupCount': 5,
|
||||
},
|
||||
|
||||
@@ -114,7 +114,7 @@ sudo systemctl restart netbox netbox-rq
|
||||
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
@@ -22,13 +22,3 @@ Each cable may be assigned a type, label, length, and color. Each cable is also
|
||||
## Tracing Cables
|
||||
|
||||
A cable may be traced from either of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user.
|
||||
|
||||
In the example below, three individual cables comprise a path between devices A and D:
|
||||
|
||||

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -20,7 +20,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
|
||||
* Selection: A selection of one of several pre-defined custom choices
|
||||
* Multiple selection: A selection field which supports the assignment of multiple values
|
||||
|
||||
Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||
Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||
|
||||
Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields.
|
||||
|
||||
|
||||
@@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
|
||||
## Link Groups
|
||||
|
||||
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
|
||||
|
||||
## Table Columns
|
||||
|
||||
Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL.
|
||||
|
||||
@@ -81,16 +81,3 @@ If no body template is specified, the request body will be populated with a JSON
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Webhooks
|
||||
|
||||
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "status",
|
||||
"value": "active"
|
||||
}
|
||||
```
|
||||
|
||||
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
|
||||
|
||||
@@ -12,3 +12,5 @@ NetBox models these redundancy groups by protocol and group ID. Each group may o
|
||||
## FHRP Group Assignments
|
||||
|
||||
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
|
||||
|
||||
Interfaces are assigned to FHRP groups under the interface detail view.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Plugin Development
|
||||
|
||||
!!! info "Help Improve the NetBox Plugins Framework!"
|
||||
We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338).
|
||||
|
||||
This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox.
|
||||
|
||||
Plugins can do a lot, including:
|
||||
|
||||
@@ -81,13 +81,16 @@ The following condition will evaluate as true:
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "status",
|
||||
"attr": "status.value",
|
||||
"value": ["planned", "staging"],
|
||||
"op": "in",
|
||||
"negate": true
|
||||
}
|
||||
```
|
||||
|
||||
!!! note "Evaluating static choice fields"
|
||||
Pay close attention when evaluating static choice fields, such as the `status` field above. These fields typically render as a dictionary specifying both the field's raw value (`value`) and its human-friendly label (`label`). be sure to specify on which of these you want to match.
|
||||
|
||||
## Condition Sets
|
||||
|
||||
Multiple conditions can be combined into nested sets using AND or OR logic. This is done by declaring a JSON object with a single key (`and` or `or`) containing a list of condition objects and/or child condition sets.
|
||||
@@ -102,7 +105,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status",
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Release Notes
|
||||
|
||||
Listed below are the major features introduced in each NetBox release. For more detail on a specific release train, see its individual release notes page.
|
||||
NetBox releases are numbered as major, minor, and patch releases. For example, version 3.1.0 is a minor release, and v3.1.5 is a patch release. Briefly, these can be described as follows:
|
||||
|
||||
* **Major** - Introduces or removes an entire API or other core functionality
|
||||
* **Minor** - Implements major new features but may include breaking changes for API consumers or other integrations
|
||||
* **Patch** - A maintenance release which fixes bugs and may introduce backward-compatible enhancements
|
||||
|
||||
Minor releases are published in April, August, and December of each calendar year. Patch releases are published as needed to address bugs and fulfill minor feature requests, typically around every one to two weeks.
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.1](./version-3.1.md) (December 2021)
|
||||
|
||||
|
||||
@@ -1,10 +1,72 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## v3.0.10 (FUTURE)
|
||||
## v3.0.12 (2021-12-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7751](https://github.com/netbox-community/netbox/issues/7751) - Get API user from LDAP only when `FIND_GROUP_PERMS` is enabled
|
||||
* [#7885](https://github.com/netbox-community/netbox/issues/7885) - Linkify VLAN name in VLANs table
|
||||
* [#7892](https://github.com/netbox-community/netbox/issues/7892) - Add L22-30 power port & outlet types
|
||||
* [#7932](https://github.com/netbox-community/netbox/issues/7932) - Improve performance of the "quick find" function
|
||||
* [#7941](https://github.com/netbox-community/netbox/issues/7941) - Add multi-standard ITA power outlet type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7823](https://github.com/netbox-community/netbox/issues/7823) - Fix issue where `return_url` is not honored when 'Save & Continue' button is present
|
||||
* [#7981](https://github.com/netbox-community/netbox/issues/7981) - Fix Markdown sanitization regex
|
||||
|
||||
---
|
||||
|
||||
## v3.0.11 (2021-11-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#2101](https://github.com/netbox-community/netbox/issues/2101) - Add missing `q` filters for necessary models
|
||||
* [#7424](https://github.com/netbox-community/netbox/issues/7424) - Add virtual chassis filters for device components
|
||||
* [#7531](https://github.com/netbox-community/netbox/issues/7531) - Add Markdown support for strikethrough formatting
|
||||
* [#7542](https://github.com/netbox-community/netbox/issues/7542) - Add optional VLAN group column to prefixes table
|
||||
* [#7803](https://github.com/netbox-community/netbox/issues/7803) - Improve live reloading of custom scripts
|
||||
* [#7810](https://github.com/netbox-community/netbox/issues/7810) - Add IEEE 802.15.1 interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7399](https://github.com/netbox-community/netbox/issues/7399) - Fix excessive CPU utilization when `AUTH_LDAP_FIND_GROUP_PERMS` is enabled
|
||||
* [#7657](https://github.com/netbox-community/netbox/issues/7657) - Make change logging middleware thread-safe
|
||||
* [#7720](https://github.com/netbox-community/netbox/issues/7720) - Fix initialization of custom script MultiObjectVar field with multiple values
|
||||
* [#7729](https://github.com/netbox-community/netbox/issues/7729) - Fix permissions evaluation when displaying VLAN group VLANs table
|
||||
* [#7739](https://github.com/netbox-community/netbox/issues/7739) - Fix exception when tracing cable across circuit with no far end termination
|
||||
* [#7813](https://github.com/netbox-community/netbox/issues/7813) - Fix handling of errors during export template rendering
|
||||
* [#7851](https://github.com/netbox-community/netbox/issues/7851) - Add missing cluster name filter for virtual machines
|
||||
* [#7857](https://github.com/netbox-community/netbox/issues/7857) - Fix ordering IP addresses by assignment status
|
||||
* [#7859](https://github.com/netbox-community/netbox/issues/7859) - Fix styling of form widgets under cable connection views
|
||||
* [#7864](https://github.com/netbox-community/netbox/issues/7864) - `power_port` can be null when creating power outlets via REST API
|
||||
* [#7865](https://github.com/netbox-community/netbox/issues/7865) - REST API should support null values for console port speeds
|
||||
|
||||
---
|
||||
|
||||
## v3.0.10 (2021-11-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7740](https://github.com/netbox-community/netbox/issues/7740) - Add mini-DIN 8 console port type
|
||||
* [#7760](https://github.com/netbox-community/netbox/issues/7760) - Add `vid` filter field to VLANs list
|
||||
* [#7767](https://github.com/netbox-community/netbox/issues/7767) - Add visual aids to interfaces table for type, enabled status
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7564](https://github.com/netbox-community/netbox/issues/7564) - Fix assignment of members to virtual chassis with initial position of zero
|
||||
* [#7701](https://github.com/netbox-community/netbox/issues/7701) - Fix conflation of assigned IP status & role in interface tables
|
||||
* [#7741](https://github.com/netbox-community/netbox/issues/7741) - Fix 404 when attaching multiple images in succession
|
||||
* [#7752](https://github.com/netbox-community/netbox/issues/7752) - Fix minimum version check under Python v3.10
|
||||
* [#7766](https://github.com/netbox-community/netbox/issues/7766) - Add missing outer dimension columns to rack table
|
||||
* [#7780](https://github.com/netbox-community/netbox/issues/7780) - Preserve multi-line values during CSV file import
|
||||
* [#7783](https://github.com/netbox-community/netbox/issues/7783) - Fix indentation of locations under site view
|
||||
* [#7788](https://github.com/netbox-community/netbox/issues/7788) - Improve XSS mitigation in Markdown renderer
|
||||
* [#7791](https://github.com/netbox-community/netbox/issues/7791) - Enable sorting device bays table by installed device status
|
||||
* [#7802](https://github.com/netbox-community/netbox/issues/7802) - Differentiate ID and VID columns in VLANs table
|
||||
* [#7808](https://github.com/netbox-community/netbox/issues/7808) - Fix reference values for content type under custom field import form
|
||||
* [#7809](https://github.com/netbox-community/netbox/issues/7809) - Add missing export template support for various models
|
||||
* [#7814](https://github.com/netbox-community/netbox/issues/7814) - Fix restriction of user & group objects in GraphQL API queries
|
||||
|
||||
---
|
||||
|
||||
@@ -305,7 +367,7 @@ More information about IP ranges is available [in the documentation](../models/i
|
||||
|
||||
#### Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963))
|
||||
|
||||
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/optional-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
|
||||
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/dynamic-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
@@ -404,7 +466,7 @@ Note that NetBox's `rqworker` process will _not_ service custom queues by defaul
|
||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||
* [#6328](https://github.com/netbox-community/netbox/issues/6328) - Build and serve documentation locally
|
||||
|
||||
### Bug Fixes (from v3.2-beta2)
|
||||
### Bug Fixes (from v3.0-beta2)
|
||||
|
||||
* [#6977](https://github.com/netbox-community/netbox/issues/6977) - Truncate global search dropdown on small screens
|
||||
* [#6979](https://github.com/netbox-community/netbox/issues/6979) - Hide "create & add another" button for circuit terminations
|
||||
|
||||
@@ -1,4 +1,181 @@
|
||||
## v3.1-beta1 (2021-11-05)
|
||||
# NetBox v3.1
|
||||
|
||||
## v3.1.7 (2022-02-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7504](https://github.com/netbox-community/netbox/issues/7504) - Include IP range data under IPAM role views
|
||||
* [#8275](https://github.com/netbox-community/netbox/issues/8275) - Introduce alternative ASDOT-formatted column for ASNs
|
||||
* [#8367](https://github.com/netbox-community/netbox/issues/8367) - Add ASNs to global search function
|
||||
* [#8368](https://github.com/netbox-community/netbox/issues/8368) - Enable controlling the order of custom script form fields with `field_order`
|
||||
* [#8381](https://github.com/netbox-community/netbox/issues/8381) - Add contacts to global search function
|
||||
* [#8462](https://github.com/netbox-community/netbox/issues/8462) - Linkify manufacturer column in device type table
|
||||
* [#8476](https://github.com/netbox-community/netbox/issues/8476) - Bring the ASN Web UI up to the standard set by other objects
|
||||
* [#8494](https://github.com/netbox-community/netbox/issues/8494) - Include locations count under tenant view
|
||||
* [#8517](https://github.com/netbox-community/netbox/issues/8517) - Render boolean custom fields as icons in object tables
|
||||
* [#8530](https://github.com/netbox-community/netbox/issues/8530) - Indicate CSV or YAML as format for "all data" export
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8315](https://github.com/netbox-community/netbox/issues/8315) - Fix display of NAT link for primary IPv4 address under device view
|
||||
* [#8377](https://github.com/netbox-community/netbox/issues/8377) - Fix calculation of absolute cable lengths when specified in fractional units
|
||||
* [#8425](https://github.com/netbox-community/netbox/issues/8425) - Fix exception when viewing change list/records with removed plugins
|
||||
* [#8456](https://github.com/netbox-community/netbox/issues/8456) - Fix redundant display of VRF RD in prefix view
|
||||
* [#8465](https://github.com/netbox-community/netbox/issues/8465) - Accept empty string values for Interface `rf_channel` in REST API
|
||||
* [#8498](https://github.com/netbox-community/netbox/issues/8498) - Fix display of selected content type filters in object list views
|
||||
* [#8499](https://github.com/netbox-community/netbox/issues/8499) - Content types REST API endpoint should not require model permission
|
||||
* [#8512](https://github.com/netbox-community/netbox/issues/8512) - Correct file permissions to allow execution of housekeeping script
|
||||
* [#8527](https://github.com/netbox-community/netbox/issues/8527) - Fix display of changelog retention period
|
||||
|
||||
---
|
||||
|
||||
## v3.1.6 (2022-01-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8246](https://github.com/netbox-community/netbox/issues/8246) - Show human-friendly values for commit rates in circuits table
|
||||
* [#8262](https://github.com/netbox-community/netbox/issues/8262) - Add cable count to tenant stats
|
||||
* [#8265](https://github.com/netbox-community/netbox/issues/8265) - Add Stackwise-n interface types
|
||||
* [#8293](https://github.com/netbox-community/netbox/issues/8293) - Show 4-byte ASNs in ASDOT notation
|
||||
* [#8302](https://github.com/netbox-community/netbox/issues/8302) - Linkify role column in device & VM tables
|
||||
* [#8337](https://github.com/netbox-community/netbox/issues/8337) - Enable sorting object tables by created & updated times
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8279](https://github.com/netbox-community/netbox/issues/8279) - Fix display of virtual chassis members in rack elevations
|
||||
* [#8285](https://github.com/netbox-community/netbox/issues/8285) - Fix `cluster_count` under tenant REST API serializer
|
||||
* [#8287](https://github.com/netbox-community/netbox/issues/8287) - Correct label in export template form
|
||||
* [#8301](https://github.com/netbox-community/netbox/issues/8301) - Fix delete button for various object children views
|
||||
* [#8305](https://github.com/netbox-community/netbox/issues/8305) - Fix assignment of custom field data to FHRP groups via UI
|
||||
* [#8306](https://github.com/netbox-community/netbox/issues/8306) - Redirect user to previous page after login
|
||||
* [#8314](https://github.com/netbox-community/netbox/issues/8314) - Prevent custom fields with default values from appearing as applied filters erroneously
|
||||
* [#8317](https://github.com/netbox-community/netbox/issues/8317) - Fix CSV import of multi-select custom field values
|
||||
* [#8319](https://github.com/netbox-community/netbox/issues/8319) - Custom URL fields should honor `ALLOWED_URL_SCHEMES` config parameter
|
||||
* [#8342](https://github.com/netbox-community/netbox/issues/8342) - Restore `created` & `last_updated` fields missing from several REST API serializers
|
||||
* [#8357](https://github.com/netbox-community/netbox/issues/8357) - Add missing tags field to location filter form
|
||||
* [#8358](https://github.com/netbox-community/netbox/issues/8358) - Fix inconsistent styling of custom fields on filter & bulk edit forms
|
||||
|
||||
---
|
||||
|
||||
## v3.1.5 (2022-01-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
|
||||
* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
|
||||
* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
|
||||
* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
|
||||
* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
|
||||
* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
|
||||
* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
|
||||
|
||||
---
|
||||
|
||||
## v3.1.4 (2022-01-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8192](https://github.com/netbox-community/netbox/issues/8192) - Add "add prefix" button to aggregate child prefixes view
|
||||
* [#8194](https://github.com/netbox-community/netbox/issues/8194) - Enable bulk user assignment to groups under admin UI
|
||||
* [#8197](https://github.com/netbox-community/netbox/issues/8197) - Allow filtering sites by group when connecting a cable
|
||||
* [#8210](https://github.com/netbox-community/netbox/issues/8210) - Establish `netbox/local/` as a path for local resources
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8187](https://github.com/netbox-community/netbox/issues/8187) - Fix rendering of tags column in object tables
|
||||
* [#8191](https://github.com/netbox-community/netbox/issues/8191) - Fix return URL when adding IP addresses to VM interfaces
|
||||
* [#8196](https://github.com/netbox-community/netbox/issues/8196) - Fix IndexError exception when viewing large IPv6 prefixes in UI
|
||||
* [#8201](https://github.com/netbox-community/netbox/issues/8201) - Custom integer fields should allow negative integers as minimum/maximum values
|
||||
|
||||
---
|
||||
|
||||
## v3.1.3 (2021-12-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
|
||||
* [#7600](https://github.com/netbox-community/netbox/issues/7600) - Include count of available IPs on prefix view
|
||||
* [#8034](https://github.com/netbox-community/netbox/issues/8034) - Enable specifying custom field validators during CSV import
|
||||
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
|
||||
* [#8175](https://github.com/netbox-community/netbox/issues/8175) - Display parent object when attaching an image
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads
|
||||
* [#7290](https://github.com/netbox-community/netbox/issues/7290) - Defer loading API-backed form fields
|
||||
* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts
|
||||
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
|
||||
* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger
|
||||
* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables
|
||||
* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view
|
||||
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
|
||||
* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables
|
||||
* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view
|
||||
* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers
|
||||
|
||||
---
|
||||
|
||||
## v3.1.2 (2021-12-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7661](https://github.com/netbox-community/netbox/issues/7661) - Remove forced styling of custom banners
|
||||
* [#7665](https://github.com/netbox-community/netbox/issues/7665) - Add toggle to show only available child prefixes
|
||||
* [#7999](https://github.com/netbox-community/netbox/issues/7999) - Add 6 GHz and 60 GHz wireless channels
|
||||
* [#8057](https://github.com/netbox-community/netbox/issues/8057) - Dynamic object tables using HTMX
|
||||
* [#8080](https://github.com/netbox-community/netbox/issues/8080) - Link to NAT IPs for device/VM primary IPs
|
||||
* [#8081](https://github.com/netbox-community/netbox/issues/8081) - Allow creating services directly from navigation menu
|
||||
* [#8083](https://github.com/netbox-community/netbox/issues/8083) - Removed "related devices" panel from device view
|
||||
* [#8108](https://github.com/netbox-community/netbox/issues/8108) - Improve breadcrumb links for device/VM components
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7674](https://github.com/netbox-community/netbox/issues/7674) - Fix inadvertent application of device type context to virtual machines
|
||||
* [#8074](https://github.com/netbox-community/netbox/issues/8074) - Ordering VMs by name should reference naturalized value
|
||||
* [#8077](https://github.com/netbox-community/netbox/issues/8077) - Fix exception when attaching image to location, circuit, or power panel
|
||||
* [#8078](https://github.com/netbox-community/netbox/issues/8078) - Add missing wireless models to `lsmodels()` in `nbshell`
|
||||
* [#8079](https://github.com/netbox-community/netbox/issues/8079) - Fix validation of LLDP neighbors when connected device has an asset tag
|
||||
* [#8088](https://github.com/netbox-community/netbox/issues/8088) - Improve legibility of text in labels with light-colored backgrounds
|
||||
* [#8092](https://github.com/netbox-community/netbox/issues/8092) - Rack elevations should not include device asset tags
|
||||
* [#8096](https://github.com/netbox-community/netbox/issues/8096) - Fix DataError during change logging of objects with very long string representations
|
||||
* [#8101](https://github.com/netbox-community/netbox/issues/8101) - Preserve return URL when using "create and add another" button
|
||||
* [#8102](https://github.com/netbox-community/netbox/issues/8102) - Raise validation error when attempting to assign an IP address to multiple objects
|
||||
|
||||
---
|
||||
|
||||
## v3.1.1 (2021-12-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8047](https://github.com/netbox-community/netbox/issues/8047) - Display sorting indicator in table column headers
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5869](https://github.com/netbox-community/netbox/issues/5869) - Fix permissions evaluation under available prefix/IP REST API endpoints
|
||||
* [#7519](https://github.com/netbox-community/netbox/issues/7519) - Return a 409 status for unfulfillable available prefix/IP requests
|
||||
* [#7690](https://github.com/netbox-community/netbox/issues/7690) - Fix custom field integer support for MultiValueNumberFilter
|
||||
* [#7990](https://github.com/netbox-community/netbox/issues/7990) - Fix `title` display on contact detail view
|
||||
* [#7996](https://github.com/netbox-community/netbox/issues/7996) - Show WWN field in interface creation form
|
||||
* [#8001](https://github.com/netbox-community/netbox/issues/8001) - Correct verbose name for wireless LAN group model
|
||||
* [#8003](https://github.com/netbox-community/netbox/issues/8003) - Fix cable tracing across bridged interfaces with no cable
|
||||
* [#8005](https://github.com/netbox-community/netbox/issues/8005) - Fix contact email display
|
||||
* [#8009](https://github.com/netbox-community/netbox/issues/8009) - Validate IP addresses for uniqueness when creating an FHRP group
|
||||
* [#8010](https://github.com/netbox-community/netbox/issues/8010) - Allow filtering devices by multiple serial numbers
|
||||
* [#8019](https://github.com/netbox-community/netbox/issues/8019) - Exclude metrics endpoint when `LOGIN_REQUIRED` is true
|
||||
* [#8030](https://github.com/netbox-community/netbox/issues/8030) - Validate custom field names
|
||||
* [#8033](https://github.com/netbox-community/netbox/issues/8033) - Fix display of zero values for custom integer fields in tables
|
||||
* [#8035](https://github.com/netbox-community/netbox/issues/8035) - Redirect back to parent prefix after creating IP address(es) where applicable
|
||||
* [#8038](https://github.com/netbox-community/netbox/issues/8038) - Placeholder filter should display zero integer values
|
||||
* [#8042](https://github.com/netbox-community/netbox/issues/8042) - Fix filtering cables list by site slug or rack name
|
||||
* [#8051](https://github.com/netbox-community/netbox/issues/8051) - Contact group parent assignment should not be required under REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.1.0 (2021-12-06)
|
||||
|
||||
!!! warning "PostgreSQL 10 Required"
|
||||
NetBox v3.1 requires PostgreSQL 10 or later.
|
||||
@@ -7,6 +184,8 @@
|
||||
|
||||
* The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination.
|
||||
* The `cable_peer` and `cable_peer_type` attributes of cable termination models have been renamed to `link_peer` and `link_peer_type`, respectively, to accommodate wireless links between interfaces.
|
||||
* Exported webhooks and custom fields now reference associated content types by raw string value (e.g. "dcim.site") rather than by human-friendly name.
|
||||
* The 128GFC interface type has been corrected from `128gfc-sfp28` to `128gfc-qsfp28`.
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -76,6 +255,7 @@ Support for single sign-on (SSO) authentication has been added via the [python-s
|
||||
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
||||
* [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names
|
||||
* [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices
|
||||
* [#5143](https://github.com/netbox-community/netbox/issues/5143) - Include a device's asset tag in its display value
|
||||
* [#6497](https://github.com/netbox-community/netbox/issues/6497) - Extend tag support to organizational models
|
||||
* [#6615](https://github.com/netbox-community/netbox/issues/6615) - Add filter lookups for custom fields
|
||||
* [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support
|
||||
@@ -85,6 +265,14 @@ Support for single sign-on (SSO) authentication has been added via the [python-s
|
||||
* [#7452](https://github.com/netbox-community/netbox/issues/7452) - Add `json` custom field type
|
||||
* [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views
|
||||
* [#7606](https://github.com/netbox-community/netbox/issues/7606) - Model transmit power for interfaces
|
||||
* [#7619](https://github.com/netbox-community/netbox/issues/7619) - Permit custom validation rules to be defined as plain data or dotted path to class
|
||||
* [#7761](https://github.com/netbox-community/netbox/issues/7761) - Extend cable tracing across bridged interfaces
|
||||
* [#7812](https://github.com/netbox-community/netbox/issues/7812) - Enable change logging for image attachments
|
||||
* [#7858](https://github.com/netbox-community/netbox/issues/7858) - Standardize the representation of content types across import & export functions
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7589](https://github.com/netbox-community/netbox/issues/7589) - Correct 128GFC interface type identifier
|
||||
|
||||
### Other Changes
|
||||
|
||||
@@ -123,12 +311,25 @@ Support for single sign-on (SSO) authentication has been added via the [python-s
|
||||
* tenancy.TenantGroup
|
||||
* virtualization.ClusterGroup
|
||||
* virtualization.ClusterType
|
||||
* circuits.CircuitTermination
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Cable
|
||||
* Added `tenant` field
|
||||
* dcim.ConsolePort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.ConsoleServerPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Device
|
||||
* The `display` field now includes the device's asset tag, if set
|
||||
* Added `airflow` field
|
||||
* dcim.DeviceType
|
||||
* Added `airflow` field
|
||||
* dcim.FrontPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Interface
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
@@ -143,8 +344,22 @@ Support for single sign-on (SSO) authentication has been added via the [python-s
|
||||
* Added `count_fhrp_groups` read-only field
|
||||
* dcim.Location
|
||||
* Added `tenant` field
|
||||
* dcim.PowerFeed
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.PowerOutlet
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.PowerPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.RearPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Site
|
||||
* Added `asns` relationship to ipam.ASN
|
||||
* extras.ImageAttachment
|
||||
* Added the `last_updated` field
|
||||
* extras.Webhook
|
||||
* Added the `conditions` field
|
||||
* virtualization.VMInterface
|
||||
|
||||
@@ -42,7 +42,7 @@ $ curl -X POST \
|
||||
https://netbox/api/users/tokens/provision/ \
|
||||
--data '{
|
||||
"username": "hankhill",
|
||||
"password: "I<3C3H8",
|
||||
"password": "I<3C3H8",
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
@@ -100,5 +100,5 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||
'_occupied',
|
||||
'_occupied', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@@ -4,9 +4,7 @@ from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect,
|
||||
)
|
||||
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@@ -16,7 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -55,7 +53,7 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -79,7 +77,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -93,7 +91,7 @@ class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
|
||||
@@ -6,7 +6,7 @@ from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import CustomFieldModelFilterForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
@@ -16,29 +16,22 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ProviderFilterForm(CustomFieldModelFilterForm):
|
||||
model = Provider
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['asn'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -47,8 +40,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
@@ -57,37 +49,26 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ProviderNetworkFilterForm(CustomFieldModelFilterForm):
|
||||
model = ProviderNetwork
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('provider_id',),
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class CircuitTypeFilterForm(CustomFieldModelFilterForm):
|
||||
model = CircuitType
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Circuit
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
@@ -96,22 +77,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
label=_('Type'),
|
||||
fetch_trigger='open'
|
||||
label=_('Type')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
@@ -119,8 +93,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
@@ -130,14 +103,12 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -146,8 +117,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
|
||||
@@ -19,7 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ProviderForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@@ -53,7 +53,7 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ProviderNetworkForm(CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
@@ -73,7 +73,7 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class CircuitTypeForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@@ -87,7 +87,7 @@ class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ from circuits.choices import *
|
||||
from dcim.models import LinkTermination
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
@@ -35,8 +34,6 @@ class CircuitType(OrganizationalModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -123,8 +120,6 @@ class Circuit(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
]
|
||||
@@ -195,8 +190,6 @@ class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
@@ -59,8 +59,6 @@ class Provider(PrimaryModel):
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
]
|
||||
@@ -97,8 +95,6 @@ class ProviderNetwork(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'name')
|
||||
constraints = (
|
||||
|
||||
@@ -22,11 +22,32 @@ CIRCUITTERMINATION_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
#
|
||||
# Table columns
|
||||
#
|
||||
|
||||
|
||||
class CommitRateColumn(tables.TemplateColumn):
|
||||
"""
|
||||
Humanize the commit rate in the column view
|
||||
"""
|
||||
|
||||
template_code = """
|
||||
{% load helpers %}
|
||||
{{ record.commit_rate|humanize_speed }}
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(template_code=self.template_code, *args, **kwargs)
|
||||
|
||||
def value(self, value):
|
||||
return str(value) if value else None
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@@ -45,7 +66,7 @@ class ProviderTable(BaseTable):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||
'comments', 'tags',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
@@ -69,7 +90,7 @@ class ProviderNetworkTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ProviderNetwork
|
||||
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
@@ -92,7 +113,7 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
@@ -119,6 +140,7 @@ class CircuitTable(BaseTable):
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side Z'
|
||||
)
|
||||
commit_rate = CommitRateColumn()
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
@@ -128,7 +150,7 @@ class CircuitTable(BaseTable):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags',
|
||||
'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
||||
@@ -340,7 +340,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -219,7 +219,7 @@ class RackReservationSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = [
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
@@ -352,7 +352,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -524,7 +525,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -548,7 +549,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -571,7 +572,8 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -619,7 +621,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -760,7 +762,7 @@ class CableSerializer(PrimaryModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
'tags', 'custom_fields',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def _get_termination(self, obj, side):
|
||||
@@ -854,7 +856,10 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -873,7 +878,10 @@ class PowerPanelSerializer(PrimaryModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
@@ -15,14 +15,14 @@ from circuits.models import Circuit
|
||||
from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN, ASN
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.exceptions import ServiceUnavailable
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.config import get_config
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related, decode_dict
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
@@ -501,7 +501,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||
continue
|
||||
try:
|
||||
response[method] = decode_dict(getattr(d, method)())
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
|
||||
@@ -204,6 +204,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_RJ11 = 'rj-11'
|
||||
TYPE_RJ12 = 'rj-12'
|
||||
TYPE_RJ45 = 'rj-45'
|
||||
TYPE_MINI_DIN_8 = 'mini-din-8'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
@@ -221,6 +222,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_RJ11, 'RJ-11'),
|
||||
(TYPE_RJ12, 'RJ-12'),
|
||||
(TYPE_RJ45, 'RJ-45'),
|
||||
(TYPE_MINI_DIN_8, 'Mini-DIN 8'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
@@ -329,6 +331,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560P = 'nema-l15-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
TYPE_NEMA_L2230P = 'nema-l22-30p'
|
||||
# California style
|
||||
TYPE_CS6361C = 'cs6361c'
|
||||
TYPE_CS6365C = 'cs6365c'
|
||||
@@ -434,6 +437,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6361C, 'CS6361C'),
|
||||
@@ -445,7 +449,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
)),
|
||||
('International/ITA', (
|
||||
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/6)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/4)'),
|
||||
(TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
@@ -550,6 +554,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560R = 'nema-l15-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
TYPE_NEMA_L2230R = 'nema-l22-30r'
|
||||
# California style
|
||||
TYPE_CS6360C = 'CS6360C'
|
||||
TYPE_CS6364C = 'CS6364C'
|
||||
@@ -569,6 +574,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_ITA_M = 'ita-m'
|
||||
TYPE_ITA_N = 'ita-n'
|
||||
TYPE_ITA_O = 'ita-o'
|
||||
TYPE_ITA_MULTISTANDARD = 'ita-multistandard'
|
||||
# USB
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_MICROB = 'usb-micro-b'
|
||||
@@ -647,6 +653,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6360C, 'CS6360C'),
|
||||
@@ -657,8 +664,8 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_CS8464C, 'CS8464C'),
|
||||
)),
|
||||
('ITA/International', (
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE7/3)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/3)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
(TYPE_ITA_H, 'ITA Type H'),
|
||||
(TYPE_ITA_I, 'ITA Type I'),
|
||||
@@ -668,6 +675,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_ITA_M, 'ITA Type M (BS 546)'),
|
||||
(TYPE_ITA_N, 'ITA Type N'),
|
||||
(TYPE_ITA_O, 'ITA Type O'),
|
||||
(TYPE_ITA_MULTISTANDARD, 'ITA Multistandard'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
@@ -757,6 +765,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
@@ -780,7 +789,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
|
||||
|
||||
# InfiniBand
|
||||
TYPE_INFINIBAND_SDR = 'infiniband-sdr'
|
||||
@@ -807,6 +816,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||
TYPE_STACKWISE80 = 'cisco-stackwise-80'
|
||||
TYPE_STACKWISE160 = 'cisco-stackwise-160'
|
||||
TYPE_STACKWISE320 = 'cisco-stackwise-320'
|
||||
TYPE_STACKWISE480 = 'cisco-stackwise-480'
|
||||
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||
@@ -869,6 +882,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -940,6 +954,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||
(TYPE_STACKWISE80, 'Cisco StackWise-80'),
|
||||
(TYPE_STACKWISE160, 'Cisco StackWise-160'),
|
||||
(TYPE_STACKWISE320, 'Cisco StackWise-320'),
|
||||
(TYPE_STACKWISE480, 'Cisco StackWise-480'),
|
||||
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||
|
||||
@@ -718,7 +718,7 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
||||
field_name='interfaces__mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
serial = django_filters.CharFilter(
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
@@ -876,6 +876,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual Chassis (ID)'
|
||||
)
|
||||
virtual_chassis = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis__name',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Virtual Chassis',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -1247,7 +1258,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__rack_id'
|
||||
)
|
||||
rack = MultiValueNumberFilter(
|
||||
rack = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__rack__name'
|
||||
)
|
||||
@@ -1255,7 +1266,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__site_id'
|
||||
)
|
||||
site = MultiValueNumberFilter(
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
@@ -1416,6 +1427,10 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site_id'
|
||||
@@ -1438,6 +1453,15 @@ class ConnectionFilterSet(BaseFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cable__label__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from django import forms
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, form_from_model
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
|
||||
from .object_create import ComponentForm
|
||||
|
||||
__all__ = (
|
||||
@@ -23,7 +23,7 @@ __all__ = (
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
|
||||
@@ -11,8 +11,8 @@ from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
|
||||
from ipam.models import VLAN, ASN
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@@ -52,7 +52,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -70,7 +70,7 @@ class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -88,7 +88,7 @@ class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -138,7 +138,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
]
|
||||
|
||||
|
||||
class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -167,7 +167,7 @@ class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
|
||||
nullable_fields = ['parent', 'tenant', 'description']
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -184,7 +184,7 @@ class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -284,7 +284,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
]
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -309,7 +309,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -323,7 +323,7 @@ class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMod
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -351,7 +351,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
||||
nullable_fields = ['airflow']
|
||||
|
||||
|
||||
class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -373,7 +373,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -396,7 +396,7 @@ class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
|
||||
nullable_fields = ['manufacturer', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -457,7 +457,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
]
|
||||
|
||||
|
||||
class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Cable.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -513,7 +513,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -527,7 +527,7 @@ class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM
|
||||
nullable_fields = ['domain']
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -566,7 +566,7 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
||||
nullable_fields = ['location']
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -631,7 +631,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
||||
# Device component templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ConsolePortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsolePortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -650,7 +650,7 @@ class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsoleServerPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -672,7 +672,7 @@ class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class PowerPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -704,7 +704,7 @@ class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class PowerOutletTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerOutletTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -752,7 +752,7 @@ class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
self.fields['power_port'].widget.attrs['disabled'] = True
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -779,7 +779,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class FrontPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -804,7 +804,7 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class RearPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -829,7 +829,7 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class DeviceBayTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceBayTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -852,7 +852,6 @@ class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -871,7 +870,6 @@ class ConsolePortBulkEditForm(
|
||||
|
||||
class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -890,7 +888,6 @@ class ConsoleServerPortBulkEditForm(
|
||||
|
||||
class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -909,7 +906,6 @@ class PowerPortBulkEditForm(
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -948,7 +944,6 @@ class InterfaceBulkEditForm(
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
]),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1061,7 +1056,6 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1076,7 +1070,6 @@ class FrontPortBulkEditForm(
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1091,7 +1084,6 @@ class RearPortBulkEditForm(
|
||||
|
||||
class DeviceBayBulkEditForm(
|
||||
form_from_model(DeviceBay, ['label', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1106,7 +1098,6 @@ class DeviceBayBulkEditForm(
|
||||
|
||||
class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
|
||||
@@ -3,7 +3,7 @@ from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'ConnectCableToCircuitTerminationForm',
|
||||
@@ -18,7 +18,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
|
||||
"""
|
||||
Base form for connecting a Cable to a Device component
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -38,7 +38,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
@@ -78,9 +78,9 @@ class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
'tags',
|
||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_rack',
|
||||
'termination_b_device', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect,
|
||||
@@ -171,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
|
||||
termination_b_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
@@ -182,7 +182,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFi
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -193,7 +193,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFi
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_circuit = DynamicModelChoiceField(
|
||||
@@ -217,12 +217,11 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFi
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
'tags',
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
|
||||
'termination_b_circuit', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
@@ -230,13 +229,13 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFi
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -247,7 +246,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelF
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
@@ -280,11 +279,11 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelF
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group',
|
||||
'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_location',
|
||||
'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label',
|
||||
'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterFor
|
||||
from ipam.models import ASN
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import (
|
||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from wireless.choices import *
|
||||
@@ -47,15 +47,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
field_order = [
|
||||
'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
class DeviceComponentFilterForm(CustomFieldModelFilterForm):
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@@ -65,14 +57,12 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -81,8 +71,7 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -90,8 +79,12 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual Chassis')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -99,58 +92,40 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
'virtual_chassis_id': '$virtual_chassis_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class RegionFilterForm(CustomFieldModelFilterForm):
|
||||
model = Region
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent region')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class SiteGroupFilterForm(CustomFieldModelFilterForm):
|
||||
model = SiteGroup
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent group')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Site
|
||||
field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id', 'asn_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['status', 'region_id', 'group_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['asn_id']
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
@@ -159,47 +134,37 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
label=_('ASNs'),
|
||||
fetch_trigger='open'
|
||||
label=_('ASNs')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Location
|
||||
field_groups = [
|
||||
['q'],
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'parent_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -208,8 +173,7 @@ class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilt
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -218,25 +182,18 @@ class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilt
|
||||
'region_id': '$region_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Parent'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class RackRoleFilterForm(CustomFieldModelFilterForm):
|
||||
model = RackRole
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Rack
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
@@ -244,16 +201,10 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
['type', 'width', 'serial', 'asset_tag'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -261,8 +212,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -271,8 +221,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
@@ -293,8 +242,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
label=_('Role')
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@@ -306,10 +254,6 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
|
||||
|
||||
class RackElevationFilterForm(RackFilterForm):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id',
|
||||
'tenant_id',
|
||||
]
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label=_('Rack'),
|
||||
@@ -317,30 +261,22 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = RackReservation
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['user_id'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -348,15 +284,13 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.prefetch_related('site'),
|
||||
required=False,
|
||||
label=_('Location'),
|
||||
null_option='None',
|
||||
fetch_trigger='open'
|
||||
null_option='None'
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
@@ -364,39 +298,27 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ManufacturerFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ManufacturerFilterForm(CustomFieldModelFilterForm):
|
||||
model = Manufacturer
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class DeviceTypeFilterForm(CustomFieldModelFilterForm):
|
||||
model = DeviceType
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['manufacturer_id', 'subdevice_role', 'airflow'],
|
||||
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
subdevice_role = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(SubdeviceRoleChoices),
|
||||
@@ -453,38 +375,23 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class DeviceRoleFilterForm(CustomFieldModelFilterForm):
|
||||
model = DeviceRole
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PlatformFilterForm(CustomFieldModelFilterForm):
|
||||
model = Platform
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id',
|
||||
'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'],
|
||||
@@ -496,22 +403,15 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
|
||||
],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -520,8 +420,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -530,8 +429,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -541,20 +439,17 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_('Rack'),
|
||||
fetch_trigger='open'
|
||||
label=_('Rack')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
label=_('Role')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
@@ -562,15 +457,13 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer_id'
|
||||
},
|
||||
label=_('Model'),
|
||||
fetch_trigger='open'
|
||||
label=_('Model')
|
||||
)
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Platform'),
|
||||
fetch_trigger='open'
|
||||
label=_('Platform')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
@@ -651,30 +544,22 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = VirtualChassis
|
||||
field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -683,30 +568,23 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Cable
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['site_id', 'rack_id', 'device_id'],
|
||||
['type', 'status', 'color'],
|
||||
['type', 'status', 'color', 'length', 'length_unit'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -714,8 +592,7 @@ class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterF
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -724,8 +601,17 @@ class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterF
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
}
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'tenant_id': '$tenant_id',
|
||||
'rack_id': '$rack_id',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
@@ -740,42 +626,31 @@ class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterF
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'tenant_id': '$tenant_id',
|
||||
'rack_id': '$rack_id',
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
length = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
length_unit = forms.ChoiceField(
|
||||
choices=add_blank_choice(CableLengthUnitChoices),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PowerPanelFilterForm(CustomFieldModelFilterForm):
|
||||
model = PowerPanel
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('region_id', 'site_group_id', 'site_id', 'location_id')
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -784,8 +659,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -794,13 +668,12 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PowerFeedFilterForm(CustomFieldModelFilterForm):
|
||||
model = PowerFeed
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
@@ -808,22 +681,15 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
['power_panel_id', 'rack_id'],
|
||||
['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -831,8 +697,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
power_panel_id = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
@@ -841,8 +706,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Power panel'),
|
||||
fetch_trigger='open'
|
||||
label=_('Power panel')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -851,8 +715,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Rack'),
|
||||
fetch_trigger='open'
|
||||
label=_('Rack')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
@@ -895,7 +758,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -915,7 +778,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -935,7 +798,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -950,7 +813,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -966,7 +829,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
kind = forms.MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
@@ -1031,7 +894,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1050,7 +913,7 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
@@ -1068,7 +931,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1078,13 +941,12 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@@ -1105,12 +967,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1118,8 +979,7 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1127,17 +987,15 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class PowerConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1145,8 +1003,7 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1154,17 +1011,15 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class InterfaceConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1172,8 +1027,7 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1181,6 +1035,5 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ Tagged (All): Implies all VLANs are available (w/optional untagged VLAN)
|
||||
"""
|
||||
|
||||
|
||||
class RegionForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RegionForm(CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
@@ -84,7 +84,7 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class SiteGroupForm(CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
@@ -102,7 +102,7 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class SiteForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
@@ -173,7 +173,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class LocationForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -221,7 +221,7 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RackRoleForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@@ -235,7 +235,7 @@ class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class RackForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -295,22 +295,20 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class RackReservationForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -318,24 +316,21 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
units = NumericArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
@@ -349,8 +344,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
fetch_trigger='open'
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -365,7 +359,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ManufacturerForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@@ -379,7 +373,7 @@ class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceTypeForm(CustomFieldModelForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all()
|
||||
)
|
||||
@@ -418,7 +412,7 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceRoleForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
@@ -432,7 +426,7 @@ class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
]
|
||||
|
||||
|
||||
class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PlatformForm(CustomFieldModelForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
@@ -455,7 +449,7 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class DeviceForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -637,7 +631,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
||||
|
||||
|
||||
class CableForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class CableForm(TenancyForm, CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -660,7 +654,7 @@ class CableForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerPanelForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -704,7 +698,7 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerFeedForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -772,7 +766,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class VirtualChassisForm(CustomFieldModelForm):
|
||||
master = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
@@ -1005,7 +999,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConsolePortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1021,7 +1015,7 @@ class ConsolePortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConsoleServerPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1037,7 +1031,7 @@ class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1054,7 +1048,7 @@ class PowerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerOutletForm(CustomFieldModelForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
@@ -1083,7 +1077,7 @@ class PowerOutletForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@@ -1183,7 +1177,7 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
||||
|
||||
class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class FrontPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1211,7 +1205,7 @@ class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class RearPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RearPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1228,7 +1222,7 @@ class RearPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceBayForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1264,7 +1258,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
).exclude(pk=device_bay.device.pk)
|
||||
|
||||
|
||||
class InventoryItemForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class InventoryItemForm(CustomFieldModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ComponentForm(forms.Form):
|
||||
class ComponentForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
a name pattern.
|
||||
@@ -63,7 +63,7 @@ class ComponentForm(forms.Form):
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -118,12 +118,18 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
|
||||
raise forms.ValidationError({
|
||||
'initial_position': "A position must be specified for the first VC member."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign VC members
|
||||
if instance.pk:
|
||||
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||
if instance.pk and self.cleaned_data['members']:
|
||||
initial_position = self.cleaned_data.get('initial_position', 1)
|
||||
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
|
||||
member.virtual_chassis = instance
|
||||
member.vc_position = i
|
||||
@@ -136,7 +142,7 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm):
|
||||
class ComponentTemplateCreateForm(ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
|
||||
"""
|
||||
@@ -329,7 +335,7 @@ class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
class ComponentCreateForm(CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
@@ -459,12 +465,17 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
},
|
||||
label='LAG'
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
wwn = forms.CharField(
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only',
|
||||
@@ -497,15 +508,17 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
label='Untagged VLAN'
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
label='Tagged VLANs'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
|
||||
'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'wwn', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Generated by Django 3.2.8 on 2021-10-19 17:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -32,14 +30,54 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='location',
|
||||
unique_together={('site', 'parent', 'name'), ('site', 'parent', 'slug')},
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='region',
|
||||
unique_together={('parent', 'slug'), ('parent', 'name')},
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(fields=('site', 'parent', 'name'), name='dcim_location_parent_name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='sitegroup',
|
||||
unique_together={('parent', 'slug'), ('parent', 'name')},
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'name'), name='dcim_location_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(fields=('site', 'parent', 'slug'), name='dcim_location_parent_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='location',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'slug'), name='dcim_location_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_region_parent_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_region_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_region_parent_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='region',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_region_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_sitegroup_parent_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_sitegroup_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_sitegroup_parent_slug'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='sitegroup',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_sitegroup_slug'),
|
||||
),
|
||||
]
|
||||
|
||||
29
netbox/dcim/migrations/0142_rename_128gfc_qsfp28.py
Normal file
29
netbox/dcim/migrations/0142_rename_128gfc_qsfp28.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django.db import migrations
|
||||
|
||||
OLD_VALUE = '128gfc-sfp28'
|
||||
NEW_VALUE = '128gfc-qsfp28'
|
||||
|
||||
|
||||
def correct_type(apps, schema_editor):
|
||||
"""
|
||||
Correct TYPE_128GFC_QSFP28 interface type.
|
||||
"""
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
|
||||
|
||||
for model in (Interface, InterfaceTemplate):
|
||||
model.objects.filter(type=OLD_VALUE).update(type=NEW_VALUE)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0141_asn_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=correct_type,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0053_asn_model'),
|
||||
('dcim', '0142_rename_128gfc_qsfp28'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'),
|
||||
),
|
||||
]
|
||||
31
netbox/dcim/migrations/0144_fix_cable_abs_length.py
Normal file
31
netbox/dcim/migrations/0144_fix_cable_abs_length.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.db import migrations
|
||||
|
||||
from utilities.utils import to_meters
|
||||
|
||||
|
||||
def recalculate_abs_length(apps, schema_editor):
|
||||
"""
|
||||
Recalculate absolute lengths for all cables with a length and length unit defined. Fixes
|
||||
incorrectly calculated values as reported under bug #8377.
|
||||
"""
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
|
||||
cables = Cable.objects.filter(length__isnull=False).exclude(length_unit='')
|
||||
for cable in cables:
|
||||
cable._abs_length = to_meters(cable.length, cable.length_unit)
|
||||
|
||||
Cable.objects.bulk_update(cables, ['_abs_length'], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0143_remove_primary_for_related_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=recalculate_abs_length,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -5,42 +5,3 @@ from .devices import *
|
||||
from .power import *
|
||||
from .racks import *
|
||||
from .sites import *
|
||||
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'Cable',
|
||||
'CablePath',
|
||||
'LinkTermination',
|
||||
'ConsolePort',
|
||||
'ConsolePortTemplate',
|
||||
'ConsoleServerPort',
|
||||
'ConsoleServerPortTemplate',
|
||||
'Device',
|
||||
'DeviceBay',
|
||||
'DeviceBayTemplate',
|
||||
'DeviceRole',
|
||||
'DeviceType',
|
||||
'FrontPort',
|
||||
'FrontPortTemplate',
|
||||
'Interface',
|
||||
'InterfaceTemplate',
|
||||
'InventoryItem',
|
||||
'Location',
|
||||
'Manufacturer',
|
||||
'Platform',
|
||||
'PowerFeed',
|
||||
'PowerOutlet',
|
||||
'PowerOutletTemplate',
|
||||
'PowerPanel',
|
||||
'PowerPort',
|
||||
'PowerPortTemplate',
|
||||
'Rack',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RearPort',
|
||||
'RearPortTemplate',
|
||||
'Region',
|
||||
'Site',
|
||||
'SiteGroup',
|
||||
'VirtualChassis',
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_ob
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, PrimaryModel
|
||||
from utilities.fields import ColorField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import to_meters
|
||||
from .devices import Device
|
||||
from .device_components import FrontPort, RearPort
|
||||
@@ -116,8 +115,6 @@ class Cable(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['pk']
|
||||
unique_together = (
|
||||
|
||||
@@ -7,7 +7,6 @@ from dcim.constants import *
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.ordering import naturalize_interface
|
||||
from .device_components import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||
@@ -50,8 +49,6 @@ class ComponentTemplateModel(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from netbox.models import PrimaryModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from wireless.choices import *
|
||||
from wireless.utils import get_channel_attr
|
||||
@@ -65,8 +64,6 @@ class ComponentModel(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -189,15 +186,23 @@ class PathEndpoint(models.Model):
|
||||
abstract = True
|
||||
|
||||
def trace(self):
|
||||
if self._path is None:
|
||||
return []
|
||||
origin = self
|
||||
path = []
|
||||
|
||||
# Construct the complete path
|
||||
path = [self, *self._path.get_path()]
|
||||
while (len(path) + 1) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort)
|
||||
path.append(None)
|
||||
path.append(self._path.destination)
|
||||
while origin is not None:
|
||||
|
||||
if origin._path is None:
|
||||
break
|
||||
|
||||
path.extend([origin, *origin._path.get_path()])
|
||||
while (len(path) + 1) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort)
|
||||
path.append(None)
|
||||
path.append(origin._path.destination)
|
||||
|
||||
# Check for bridge interface to continue the trace
|
||||
origin = getattr(origin._path.destination, 'bridge', None)
|
||||
|
||||
# Return the path as a list of three-tuples (A termination, cable, B termination)
|
||||
return list(zip(*[iter(path)] * 3))
|
||||
|
||||
@@ -18,7 +18,6 @@ from netbox.config import ConfigItem
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from .device_components import *
|
||||
|
||||
|
||||
@@ -59,8 +58,6 @@ class Manufacturer(OrganizationalModel):
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -137,8 +134,6 @@ class DeviceType(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
]
|
||||
@@ -379,8 +374,6 @@ class DeviceRole(OrganizationalModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -431,8 +424,6 @@ class Platform(OrganizationalModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -549,7 +540,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
primary_ip4 = models.OneToOneField(
|
||||
to='ipam.IPAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='primary_ip4_for',
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Primary IPv4'
|
||||
@@ -557,7 +548,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
primary_ip6 = models.OneToOneField(
|
||||
to='ipam.IPAddress',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='primary_ip6_for',
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Primary IPv6'
|
||||
@@ -613,7 +604,9 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
if self.name and self.asset_tag:
|
||||
return f'{self.name} ({self.asset_tag})'
|
||||
elif self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
@@ -896,8 +889,6 @@ class VirtualChassis(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import ExclusionValidator
|
||||
from .device_components import LinkTermination, PathEndpoint
|
||||
|
||||
@@ -49,8 +48,6 @@ class PowerPanel(PrimaryModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = ['site', 'name']
|
||||
@@ -131,8 +128,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization', 'available_power',
|
||||
|
||||
@@ -18,7 +18,6 @@ from netbox.config import get_config
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import array_to_string
|
||||
from .device_components import PowerOutlet, PowerPort
|
||||
from .devices import Device
|
||||
@@ -56,8 +55,6 @@ class RackRole(OrganizationalModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -190,8 +187,6 @@ class Rack(PrimaryModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit',
|
||||
@@ -471,8 +466,6 @@ class RackReservation(PrimaryModel):
|
||||
max_length=200
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from dcim.fields import ASNField
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import NestedGroupModel, PrimaryModel
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'Location',
|
||||
@@ -63,11 +62,41 @@ class Region(NestedGroupModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('parent', 'name'),
|
||||
('parent', 'slug'),
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
name='dcim_region_parent_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('name',),
|
||||
name='dcim_region_name',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'slug'),
|
||||
name='dcim_region_parent_slug'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('slug',),
|
||||
name='dcim_region_slug',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
if self.parent is None:
|
||||
regions = Region.objects.exclude(pk=self.pk)
|
||||
if regions.filter(name=self.name, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
'name': 'A region with this name already exists.'
|
||||
})
|
||||
if regions.filter(slug=self.slug, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
'name': 'A region with this slug already exists.'
|
||||
})
|
||||
|
||||
super().validate_unique(exclude=exclude)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:region', args=[self.pk])
|
||||
|
||||
@@ -120,11 +149,41 @@ class SiteGroup(NestedGroupModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('parent', 'name'),
|
||||
('parent', 'slug'),
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
name='dcim_sitegroup_parent_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('name',),
|
||||
name='dcim_sitegroup_name',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'slug'),
|
||||
name='dcim_sitegroup_parent_slug'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('slug',),
|
||||
name='dcim_sitegroup_slug',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
if self.parent is None:
|
||||
site_groups = SiteGroup.objects.exclude(pk=self.pk)
|
||||
if site_groups.filter(name=self.name, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
'name': 'A site group with this name already exists.'
|
||||
})
|
||||
if site_groups.filter(slug=self.slug, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
'name': 'A site group with this slug already exists.'
|
||||
})
|
||||
|
||||
super().validate_unique(exclude=exclude)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:sitegroup', args=[self.pk])
|
||||
|
||||
@@ -259,8 +318,6 @@ class Site(PrimaryModel):
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
|
||||
@@ -338,10 +395,40 @@ class Location(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = ([
|
||||
('site', 'parent', 'name'),
|
||||
('site', 'parent', 'slug'),
|
||||
])
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('site', 'parent', 'name'),
|
||||
name='dcim_location_parent_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('site', 'name'),
|
||||
name='dcim_location_name',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('site', 'parent', 'slug'),
|
||||
name='dcim_location_parent_slug'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('site', 'slug'),
|
||||
name='dcim_location_slug',
|
||||
condition=Q(parent=None)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
if self.parent is None:
|
||||
locations = Location.objects.exclude(pk=self.pk)
|
||||
if locations.filter(name=self.name, site=self.site, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
"name": f"A location with this name in site {self.site} already exists."
|
||||
})
|
||||
if locations.filter(slug=self.slug, site=self.site, parent__isnull=True).exists():
|
||||
raise ValidationError({
|
||||
"name": f"A location with this slug in site {self.site} already exists."
|
||||
})
|
||||
|
||||
super().validate_unique(exclude=exclude)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
@@ -18,6 +18,15 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
def get_device_name(device):
|
||||
if device.virtual_chassis:
|
||||
return f'{device.virtual_chassis.name}:{device.vc_position}'
|
||||
elif device.name:
|
||||
return device.name
|
||||
else:
|
||||
return str(device.device_type)
|
||||
|
||||
|
||||
class RackElevationSVG:
|
||||
"""
|
||||
Use this class to render a rack elevation as an SVG image.
|
||||
@@ -85,7 +94,7 @@ class RackElevationSVG:
|
||||
return drawing
|
||||
|
||||
def _draw_device_front(self, drawing, device, start, end, text):
|
||||
name = str(device)
|
||||
name = get_device_name(device)
|
||||
if device.devicebay_count:
|
||||
name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
|
||||
|
||||
@@ -120,7 +129,7 @@ class RackElevationSVG:
|
||||
rect = drawing.rect(start, end, class_="slot blocked")
|
||||
rect.set_desc(self._get_device_description(device))
|
||||
drawing.add(rect)
|
||||
drawing.add(drawing.text(str(device), insert=text))
|
||||
drawing.add(drawing.text(get_device_name(device), insert=text))
|
||||
|
||||
# Embed rear device type image if one exists
|
||||
if self.include_images and device.device_type.rear_image:
|
||||
@@ -132,9 +141,9 @@ class RackElevationSVG:
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
drawing.add(image)
|
||||
drawing.add(drawing.text(str(device), insert=text, stroke='black',
|
||||
drawing.add(drawing.text(get_device_name(device), insert=text, stroke='black',
|
||||
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
|
||||
drawing.add(drawing.text(str(device), insert=text, fill='white', class_='device-image-label'))
|
||||
drawing.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
|
||||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
@@ -478,15 +487,16 @@ class CableTraceSVG:
|
||||
parent_objects.append(parent_object)
|
||||
|
||||
# Near end termination
|
||||
termination = self._draw_box(
|
||||
width=self.width * .8,
|
||||
color=self._get_color(near_end),
|
||||
url=near_end.get_absolute_url(),
|
||||
labels=self._get_labels(near_end),
|
||||
y_indent=PADDING,
|
||||
radius=5
|
||||
)
|
||||
terminations.append(termination)
|
||||
if near_end is not None:
|
||||
termination = self._draw_box(
|
||||
width=self.width * .8,
|
||||
color=self._get_color(near_end),
|
||||
url=near_end.get_absolute_url(),
|
||||
labels=self._get_labels(near_end),
|
||||
y_indent=PADDING,
|
||||
radius=5
|
||||
)
|
||||
terminations.append(termination)
|
||||
|
||||
# Connector (a Cable or WirelessLink)
|
||||
if connector is not None:
|
||||
|
||||
@@ -45,7 +45,7 @@ class CableTable(BaseTable):
|
||||
tenant = TenantColumn()
|
||||
length = TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by='_abs_length'
|
||||
order_by=('_abs_length', 'length_unit')
|
||||
)
|
||||
color = ColorColumn()
|
||||
tags = TagColumn(
|
||||
@@ -56,7 +56,7 @@ class CableTable(BaseTable):
|
||||
model = Cable
|
||||
fields = (
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||
'status', 'type', 'tenant', 'color', 'length', 'tags',
|
||||
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||
|
||||
@@ -49,6 +49,14 @@ def get_cabletermination_row_class(record):
|
||||
return ''
|
||||
|
||||
|
||||
def get_interface_row_class(record):
|
||||
if not record.enabled:
|
||||
return 'danger'
|
||||
elif record.is_virtual:
|
||||
return 'primary'
|
||||
return get_cabletermination_row_class(record)
|
||||
|
||||
|
||||
def get_interface_state_attribute(record):
|
||||
"""
|
||||
Get interface enabled state as string to attach to <tr/> DOM element.
|
||||
@@ -89,7 +97,7 @@ class DeviceRoleTable(BaseTable):
|
||||
model = DeviceRole
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
|
||||
'actions',
|
||||
'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||
|
||||
@@ -122,7 +130,7 @@ class PlatformTable(BaseTable):
|
||||
model = Platform
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'description', 'tags', 'actions',
|
||||
'description', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
||||
@@ -196,7 +204,8 @@ class DeviceTable(BaseTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||
@@ -252,7 +261,7 @@ class CableTerminationTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
cable_color = ColorColumn(
|
||||
accessor='cable.color',
|
||||
accessor='cable__color',
|
||||
orderable=False,
|
||||
verbose_name='Cable Color'
|
||||
)
|
||||
@@ -267,7 +276,7 @@ class CableTerminationTable(BaseTable):
|
||||
|
||||
class PathEndpointTable(CableTerminationTable):
|
||||
connection = TemplateColumn(
|
||||
accessor='_path.last_node',
|
||||
accessor='_path__last_node',
|
||||
template_code=LINKTERMINATION,
|
||||
verbose_name='Connection',
|
||||
orderable=False
|
||||
@@ -289,7 +298,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'link_peer', 'connection', 'tags',
|
||||
'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
|
||||
@@ -333,7 +342,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'link_peer', 'connection', 'tags',
|
||||
'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
|
||||
@@ -378,7 +387,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw',
|
||||
'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
||||
'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
@@ -429,7 +438,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected',
|
||||
'cable', 'cable_color', 'link_peer', 'connection', 'tags',
|
||||
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
@@ -467,6 +476,12 @@ class BaseInterfaceTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='IP Addresses'
|
||||
)
|
||||
fhrp_groups = tables.TemplateColumn(
|
||||
accessor=Accessor('fhrp_group_assignments'),
|
||||
template_code=INTERFACE_FHRPGROUPS,
|
||||
orderable=False,
|
||||
verbose_name='FHRP Groups'
|
||||
)
|
||||
untagged_vlan = tables.Column(linkify=True)
|
||||
tagged_vlans = TemplateColumn(
|
||||
template_code=INTERFACE_TAGGED_VLANS,
|
||||
@@ -501,14 +516,14 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
||||
'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
||||
class DeviceInterfaceTable(InterfaceTable):
|
||||
name = tables.TemplateColumn(
|
||||
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}drag-horizontal-variant'
|
||||
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}reorder-horizontal'
|
||||
'{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet'
|
||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||
order_by=Accessor('_name'),
|
||||
@@ -534,9 +549,9 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode',
|
||||
'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
||||
'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer',
|
||||
'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
@@ -544,7 +559,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'cable', 'connection', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class,
|
||||
'class': get_interface_row_class,
|
||||
'data-name': lambda record: record.name,
|
||||
'data-enabled': get_interface_state_attribute,
|
||||
}
|
||||
@@ -572,7 +587,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
|
||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
@@ -623,7 +638,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'link_peer', 'tags',
|
||||
'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||
|
||||
@@ -663,7 +678,8 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
}
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=DEVICEBAY_STATUS
|
||||
template_code=DEVICEBAY_STATUS,
|
||||
order_by=Accessor('installed_device__status')
|
||||
)
|
||||
installed_device = tables.Column(
|
||||
linkify=True
|
||||
@@ -674,7 +690,11 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||
|
||||
|
||||
@@ -721,7 +741,7 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags',
|
||||
'discovered', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
@@ -773,5 +793,5 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
@@ -50,7 +50,7 @@ class ManufacturerTable(BaseTable):
|
||||
model = Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
|
||||
'actions',
|
||||
'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
@@ -67,6 +67,9 @@ class DeviceTypeTable(BaseTable):
|
||||
linkify=True,
|
||||
verbose_name='Device Type'
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
is_full_depth = BooleanColumn(
|
||||
verbose_name='Full Depth'
|
||||
)
|
||||
@@ -84,7 +87,7 @@ class DeviceTypeTable(BaseTable):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'airflow', 'comments', 'instance_count', 'tags',
|
||||
'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||
@@ -111,8 +114,7 @@ class ComponentTemplateTable(BaseTable):
|
||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
model=ConsolePortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_consoleports'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -124,8 +126,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
model=ConsoleServerPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_consoleserverports'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -137,8 +138,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
model=PowerPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_powerports'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -150,8 +150,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
model=PowerOutletTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_poweroutlets'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -166,8 +165,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=InterfaceTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_interfaces'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -183,8 +181,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=FrontPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_frontports'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -197,8 +194,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=RearPortTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_rearports'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
@@ -210,8 +206,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
model=DeviceBayTemplate,
|
||||
buttons=('edit', 'delete'),
|
||||
return_url_extra='%23tab_devicebays'
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
|
||||
@@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class PowerFeedTable(CableTerminationTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
|
||||
'comments', 'tags',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
|
||||
@@ -31,7 +31,10 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -75,16 +78,25 @@ class RackTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
outer_width = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
|
||||
verbose_name='Outer Width'
|
||||
)
|
||||
outer_depth = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
||||
verbose_name='Outer Depth'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
|
||||
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
||||
'get_power_utilization', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'get_utilization', 'get_power_utilization',
|
||||
'get_utilization',
|
||||
)
|
||||
|
||||
|
||||
@@ -119,7 +131,7 @@ class RackReservationTable(BaseTable):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'actions',
|
||||
'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
|
||||
|
||||
@@ -36,7 +36,7 @@ class RegionTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class SiteGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class SiteTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
asn_count = LinkedCountColumn(
|
||||
accessor=tables.A('asns.count'),
|
||||
accessor=tables.A('asns__count'),
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name='ASNs'
|
||||
@@ -98,7 +98,7 @@ class SiteTable(BaseTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
||||
|
||||
@@ -138,6 +138,6 @@ class LocationTable(BaseTable):
|
||||
model = Location
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
|
||||
'actions',
|
||||
'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
||||
|
||||
@@ -9,7 +9,8 @@ LINKTERMINATION = """
|
||||
"""
|
||||
|
||||
CABLE_LENGTH = """
|
||||
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% endif %}
|
||||
{% load helpers %}
|
||||
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
||||
"""
|
||||
|
||||
CABLE_TERMINATION_PARENT = """
|
||||
@@ -40,17 +41,21 @@ DEVICEBAY_STATUS = """
|
||||
|
||||
INTERFACE_IPADDRESSES = """
|
||||
<div class="table-badge-group">
|
||||
{% for ip in record.ip_addresses.all %}
|
||||
<a
|
||||
class="table-badge{% if ip.status != 'active' %} badge bg-{{ ip.get_status_class }}{% elif ip.role %} badge bg-{{ ip.get_role_class }}{% endif %}"
|
||||
href="{{ ip.get_absolute_url }}"
|
||||
{% if ip.status != 'active'%}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}"
|
||||
{% elif ip.role %}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_role_display }}"
|
||||
{% endif %}
|
||||
>
|
||||
{{ ip }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% for ip in record.ip_addresses.all %}
|
||||
{% if ip.status != 'active' %}
|
||||
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_class }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
|
||||
{% else %}
|
||||
<a href="{{ ip.get_absolute_url }}" class="table-badge">{{ ip }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
INTERFACE_FHRPGROUPS = """
|
||||
<div class="table-badge-group">
|
||||
{% for assignment in value.all %}
|
||||
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group.get_protocol_display }}: {{ assignment.group.group_id }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from dcim.models import *
|
||||
from ipam.models import ASN, RIR, VLAN
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
from wireless.choices import WirelessChannelChoices
|
||||
from wireless.models import WirelessLAN
|
||||
|
||||
|
||||
@@ -595,6 +596,12 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
|
||||
power_port_templates = (
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
|
||||
)
|
||||
PowerPortTemplate.objects.bulk_create(power_port_templates)
|
||||
|
||||
power_outlet_templates = (
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'),
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'),
|
||||
@@ -606,14 +613,17 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 4',
|
||||
'power_port': power_port_templates[0].pk,
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 5',
|
||||
'power_port': power_port_templates[1].pk,
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 6',
|
||||
'power_port': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1044,14 +1054,17 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 4',
|
||||
'speed': 9600,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 5',
|
||||
'speed': 115200,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 6',
|
||||
'speed': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1083,14 +1096,17 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 4',
|
||||
'speed': 9600,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 5',
|
||||
'speed': 115200,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 6',
|
||||
'speed': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1150,6 +1166,12 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
|
||||
device = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Device 1', site=site)
|
||||
|
||||
power_ports = (
|
||||
PowerPort(device=device, name='Power Port 1'),
|
||||
PowerPort(device=device, name='Power Port 2'),
|
||||
)
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
|
||||
power_outlets = (
|
||||
PowerOutlet(device=device, name='Power Outlet 1'),
|
||||
PowerOutlet(device=device, name='Power Outlet 2'),
|
||||
@@ -1161,14 +1183,17 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 4',
|
||||
'power_port': power_ports[0].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 5',
|
||||
'power_port': power_ports[1].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 6',
|
||||
'power_port': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1215,10 +1240,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'name': 'Interface 4',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'tx_power': 10,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
@@ -1226,10 +1249,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'bridge': interfaces[0].pk,
|
||||
'tx_power': 10,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
@@ -1237,10 +1258,24 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'type': 'virtual',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'parent': interfaces[1].pk,
|
||||
'tx_power': 10,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Interface 7',
|
||||
'type': InterfaceTypeChoices.TYPE_80211A,
|
||||
'tx_power': 10,
|
||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||
'rf_channel': WirelessChannelChoices.CHANNEL_5G_32,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Interface 8',
|
||||
'type': InterfaceTypeChoices.TYPE_80211A,
|
||||
'tx_power': 10,
|
||||
'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
|
||||
'rf_channel': "",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1548,7 +1583,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualChassis
|
||||
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -1420,10 +1420,10 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_serial(self):
|
||||
params = {'serial': 'ABC'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': 'abc'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': ['ABC', 'DEF']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'serial': ['abc', 'def']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_has_primary_ip(self):
|
||||
params = {'has_primary_ip': 'true'}
|
||||
@@ -2073,6 +2073,11 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
# VirtualChassis assignment for filtering
|
||||
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
|
||||
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)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
|
||||
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
|
||||
@@ -2197,6 +2202,10 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_chassis_id(self):
|
||||
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from ipam.views import ServiceEditView
|
||||
from utilities.views import SlugRedirectView
|
||||
from . import views
|
||||
from .models import *
|
||||
@@ -233,7 +232,6 @@ urlpatterns = [
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
path('devices/<int:device>/services/assign/', ServiceEditView.as_view(), name='device_service_assign'),
|
||||
|
||||
# Console ports
|
||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||
|
||||
@@ -27,44 +27,38 @@ from virtualization.models import VirtualMachine
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
from .models import (
|
||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
SiteGroup, VirtualChassis,
|
||||
)
|
||||
from .models import *
|
||||
|
||||
|
||||
class DeviceComponentsView(generic.ObjectView):
|
||||
class DeviceComponentsView(generic.ObjectChildrenView):
|
||||
queryset = Device.objects.all()
|
||||
model = None
|
||||
table = None
|
||||
|
||||
def get_components(self, request, instance):
|
||||
return self.model.objects.restrict(request.user, 'view').filter(device=instance)
|
||||
def get_children(self, request, parent):
|
||||
return self.child_model.objects.restrict(request.user, 'view').filter(device=parent)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
components = self.get_components(request, instance)
|
||||
table = self.table(data=components, user=request.user)
|
||||
change_perm = f'{self.model._meta.app_label}.change_{self.model._meta.model_name}'
|
||||
delete_perm = f'{self.model._meta.app_label}.delete_{self.model._meta.model_name}'
|
||||
if request.user.has_perm(change_perm) or request.user.has_perm(delete_perm):
|
||||
table.columns.show('pk')
|
||||
paginate_table(table, request)
|
||||
|
||||
return {
|
||||
'table': table,
|
||||
'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}",
|
||||
'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
|
||||
}
|
||||
|
||||
|
||||
class DeviceTypeComponentsView(DeviceComponentsView):
|
||||
queryset = DeviceType.objects.all()
|
||||
template_name = 'dcim/devicetype/component_templates.html'
|
||||
viewname = None # Used for return_url resolution
|
||||
|
||||
def get_components(self, request, instance):
|
||||
return self.model.objects.restrict(request.user, 'view').filter(device_type=instance)
|
||||
def get_children(self, request, parent):
|
||||
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
if self.viewname:
|
||||
return_url = reverse(self.viewname, kwargs={'pk': instance.pk})
|
||||
else:
|
||||
return_url = instance.get_absolute_url()
|
||||
return {
|
||||
'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
|
||||
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
@@ -157,6 +151,7 @@ class RegionView(generic.ObjectView):
|
||||
parent__in=instance.get_descendants(include_self=True)
|
||||
)
|
||||
child_regions_table = tables.RegionTable(child_regions)
|
||||
child_regions_table.columns.hide('actions')
|
||||
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
region=instance
|
||||
@@ -241,6 +236,7 @@ class SiteGroupView(generic.ObjectView):
|
||||
parent__in=instance.get_descendants(include_self=True)
|
||||
)
|
||||
child_groups_table = tables.SiteGroupTable(child_groups)
|
||||
child_groups_table.columns.hide('actions')
|
||||
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
group=instance
|
||||
@@ -310,6 +306,7 @@ class SiteView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
stats = {
|
||||
'location_count': Location.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),
|
||||
@@ -803,43 +800,59 @@ class DeviceTypeView(generic.ObjectView):
|
||||
|
||||
|
||||
class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
|
||||
model = ConsolePortTemplate
|
||||
child_model = ConsolePortTemplate
|
||||
table = tables.ConsolePortTemplateTable
|
||||
filterset = filtersets.ConsolePortTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_consoleports'
|
||||
|
||||
|
||||
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
|
||||
model = ConsoleServerPortTemplate
|
||||
child_model = ConsoleServerPortTemplate
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
filterset = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_consoleserverports'
|
||||
|
||||
|
||||
class DeviceTypePowerPortsView(DeviceTypeComponentsView):
|
||||
model = PowerPortTemplate
|
||||
child_model = PowerPortTemplate
|
||||
table = tables.PowerPortTemplateTable
|
||||
filterset = filtersets.PowerPortTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_powerports'
|
||||
|
||||
|
||||
class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
|
||||
model = PowerOutletTemplate
|
||||
child_model = PowerOutletTemplate
|
||||
table = tables.PowerOutletTemplateTable
|
||||
filterset = filtersets.PowerOutletTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_poweroutlets'
|
||||
|
||||
|
||||
class DeviceTypeInterfacesView(DeviceTypeComponentsView):
|
||||
model = InterfaceTemplate
|
||||
child_model = InterfaceTemplate
|
||||
table = tables.InterfaceTemplateTable
|
||||
filterset = filtersets.InterfaceTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_interfaces'
|
||||
|
||||
|
||||
class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
|
||||
model = FrontPortTemplate
|
||||
child_model = FrontPortTemplate
|
||||
table = tables.FrontPortTemplateTable
|
||||
filterset = filtersets.FrontPortTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_frontports'
|
||||
|
||||
|
||||
class DeviceTypeRearPortsView(DeviceTypeComponentsView):
|
||||
model = RearPortTemplate
|
||||
child_model = RearPortTemplate
|
||||
table = tables.RearPortTemplateTable
|
||||
filterset = filtersets.RearPortTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_rearports'
|
||||
|
||||
|
||||
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
|
||||
model = DeviceBayTemplate
|
||||
child_model = DeviceBayTemplate
|
||||
table = tables.DeviceBayTemplateTable
|
||||
filterset = filtersets.DeviceBayTemplateFilterSet
|
||||
viewname = 'dcim:devicetype_devicebays'
|
||||
|
||||
|
||||
class DeviceTypeEditView(generic.ObjectEditView):
|
||||
@@ -1316,80 +1329,79 @@ class DeviceView(generic.ObjectView):
|
||||
# Services
|
||||
services = Service.objects.restrict(request.user, 'view').filter(device=instance)
|
||||
|
||||
# Find up to ten devices in the same site with the same functional role for quick reference.
|
||||
related_devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
site=instance.site, device_role=instance.device_role
|
||||
).exclude(
|
||||
pk=instance.pk
|
||||
).prefetch_related(
|
||||
'rack', 'device_type__manufacturer'
|
||||
)[:10]
|
||||
|
||||
return {
|
||||
'services': services,
|
||||
'vc_members': vc_members,
|
||||
'related_devices': related_devices,
|
||||
'active_tab': 'device',
|
||||
}
|
||||
|
||||
|
||||
class DeviceConsolePortsView(DeviceComponentsView):
|
||||
model = ConsolePort
|
||||
child_model = ConsolePort
|
||||
table = tables.DeviceConsolePortTable
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
template_name = 'dcim/device/consoleports.html'
|
||||
|
||||
|
||||
class DeviceConsoleServerPortsView(DeviceComponentsView):
|
||||
model = ConsoleServerPort
|
||||
child_model = ConsoleServerPort
|
||||
table = tables.DeviceConsoleServerPortTable
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
template_name = 'dcim/device/consoleserverports.html'
|
||||
|
||||
|
||||
class DevicePowerPortsView(DeviceComponentsView):
|
||||
model = PowerPort
|
||||
child_model = PowerPort
|
||||
table = tables.DevicePowerPortTable
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
template_name = 'dcim/device/powerports.html'
|
||||
|
||||
|
||||
class DevicePowerOutletsView(DeviceComponentsView):
|
||||
model = PowerOutlet
|
||||
child_model = PowerOutlet
|
||||
table = tables.DevicePowerOutletTable
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
template_name = 'dcim/device/poweroutlets.html'
|
||||
|
||||
|
||||
class DeviceInterfacesView(DeviceComponentsView):
|
||||
model = Interface
|
||||
child_model = Interface
|
||||
table = tables.DeviceInterfaceTable
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
template_name = 'dcim/device/interfaces.html'
|
||||
|
||||
def get_components(self, request, instance):
|
||||
return instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||
def get_children(self, request, parent):
|
||||
return parent.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))
|
||||
)
|
||||
|
||||
|
||||
class DeviceFrontPortsView(DeviceComponentsView):
|
||||
model = FrontPort
|
||||
child_model = FrontPort
|
||||
table = tables.DeviceFrontPortTable
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
template_name = 'dcim/device/frontports.html'
|
||||
|
||||
|
||||
class DeviceRearPortsView(DeviceComponentsView):
|
||||
model = RearPort
|
||||
child_model = RearPort
|
||||
table = tables.DeviceRearPortTable
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
template_name = 'dcim/device/rearports.html'
|
||||
|
||||
|
||||
class DeviceDeviceBaysView(DeviceComponentsView):
|
||||
model = DeviceBay
|
||||
child_model = DeviceBay
|
||||
table = tables.DeviceDeviceBayTable
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
template_name = 'dcim/device/devicebays.html'
|
||||
|
||||
|
||||
class DeviceInventoryView(DeviceComponentsView):
|
||||
model = InventoryItem
|
||||
child_model = InventoryItem
|
||||
table = tables.DeviceInventoryItemTable
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
template_name = 'dcim/device/inventory.html'
|
||||
|
||||
|
||||
@@ -2023,8 +2035,9 @@ class DeviceBayPopulateView(generic.ObjectEditView):
|
||||
device_bay.installed_device = form.cleaned_data['installed_device']
|
||||
device_bay.save()
|
||||
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||
return_url = self.get_return_url(request)
|
||||
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
return redirect(return_url)
|
||||
|
||||
return render(request, 'dcim/devicebay_populate.html', {
|
||||
'device_bay': device_bay,
|
||||
|
||||
@@ -27,11 +27,14 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
||||
('Pagination', {
|
||||
'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'),
|
||||
}),
|
||||
('Validation', {
|
||||
'fields': ('CUSTOM_VALIDATORS',),
|
||||
}),
|
||||
('NAPALM', {
|
||||
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
|
||||
}),
|
||||
('Miscellaneous', {
|
||||
'fields': ('MAINTENANCE_MODE', 'MAPS_URL'),
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
|
||||
}),
|
||||
('Config Revision', {
|
||||
'fields': ('comment',),
|
||||
|
||||
@@ -5,10 +5,10 @@ from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
|
||||
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
|
||||
NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
@@ -61,7 +61,7 @@ class WebhookSerializer(ValidatedModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
'conditions', 'ssl_verification', 'ca_file_path',
|
||||
'conditions', 'ssl_verification', 'ca_file_path', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
|
||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
|
||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -100,7 +101,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
model = CustomLink
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window',
|
||||
'button_class', 'new_window', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -118,7 +119,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
model = ExportTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
||||
'file_extension', 'as_attachment',
|
||||
'file_extension', 'as_attachment', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -132,7 +133,9 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -150,7 +153,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
model = ImageAttachment
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
||||
'image_width', 'created',
|
||||
'image_width', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -170,17 +173,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_parent(self, obj):
|
||||
|
||||
# Static mapping of models to their nested serializers
|
||||
if isinstance(obj.parent, Device):
|
||||
serializer = NestedDeviceSerializer
|
||||
elif isinstance(obj.parent, Rack):
|
||||
serializer = NestedRackSerializer
|
||||
elif isinstance(obj.parent, Site):
|
||||
serializer = NestedSiteSerializer
|
||||
else:
|
||||
raise Exception("Unexpected type of parent object for ImageAttachment")
|
||||
|
||||
serializer = get_serializer_for_model(obj.parent, prefix='Nested')
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
@@ -382,6 +383,7 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
|
||||
"""
|
||||
permission_classes = (IsAuthenticated,)
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ContentTypeSerializer
|
||||
filterset_class = filtersets.ContentTypeFilterSet
|
||||
|
||||
@@ -2,8 +2,9 @@ from contextlib import contextmanager
|
||||
|
||||
from django.db.models.signals import m2m_changed, pre_delete, post_save
|
||||
|
||||
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
|
||||
from utilities.utils import curry
|
||||
from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object
|
||||
from netbox import thread_locals
|
||||
from netbox.request_context import set_request
|
||||
from .webhooks import flush_webhooks
|
||||
|
||||
|
||||
@@ -15,12 +16,8 @@ def change_logging(request):
|
||||
|
||||
:param request: WSGIRequest object with a unique `id` set
|
||||
"""
|
||||
webhook_queue = []
|
||||
|
||||
# Curry signals receivers to pass the current request
|
||||
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
|
||||
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
|
||||
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
|
||||
set_request(request)
|
||||
thread_locals.webhook_queue = []
|
||||
|
||||
# Connect our receivers to the post_save and post_delete signals.
|
||||
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
@@ -38,5 +35,8 @@ def change_logging(request):
|
||||
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
|
||||
|
||||
# Flush queued webhooks to RQ
|
||||
flush_webhooks(webhook_queue)
|
||||
del webhook_queue
|
||||
flush_webhooks(thread_locals.webhook_queue)
|
||||
del thread_locals.webhook_queue
|
||||
|
||||
# Clear the request from thread-local storage
|
||||
set_request(None)
|
||||
|
||||
@@ -28,6 +28,10 @@ __all__ = (
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
@@ -40,30 +44,81 @@ class WebhookFilterSet(BaseFilterSet):
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(payload_url__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(label__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(link_text__icontains=value) |
|
||||
Q(link_url__icontains=value) |
|
||||
Q(group_name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'content_type', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFilter()
|
||||
content_type = ContentTypeFilter()
|
||||
|
||||
@@ -71,6 +126,11 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
|
||||
class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
|
||||
@@ -4,9 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
|
||||
)
|
||||
from utilities.forms import BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextBulkEditForm',
|
||||
@@ -19,7 +17,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CustomField.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -39,14 +37,14 @@ class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class CustomLinkBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CustomLink.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('custom_links'),
|
||||
required=False
|
||||
)
|
||||
new_window = forms.NullBooleanField(
|
||||
@@ -66,14 +64,14 @@ class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ExportTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ExportTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('export_templates'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
@@ -97,7 +95,7 @@ class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ['description', 'mime_type', 'file_extension']
|
||||
|
||||
|
||||
class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class WebhookBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Webhook.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -140,7 +138,7 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ['secret', 'conditions', 'ca_file_path']
|
||||
|
||||
|
||||
class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class TagBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -157,7 +155,7 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ConfigContextBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConfigContext.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -181,7 +179,7 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
]
|
||||
|
||||
|
||||
class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class JournalEntryBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=JournalEntry.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
|
||||
@@ -3,9 +3,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
|
||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CustomFieldCSVForm',
|
||||
@@ -22,6 +23,10 @@ class CustomFieldCSVForm(CSVModelForm):
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
help_text="One or more assigned object types"
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=CustomFieldTypeChoices,
|
||||
help_text='Field data type (e.g. text, integer, etc.)'
|
||||
)
|
||||
choices = SimpleArrayField(
|
||||
base_field=forms.CharField(),
|
||||
required=False,
|
||||
@@ -32,7 +37,7 @@ class CustomFieldCSVForm(CSVModelForm):
|
||||
model = CustomField
|
||||
fields = (
|
||||
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
|
||||
'choices', 'weight',
|
||||
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db.models import Q
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from utilities.forms import BulkEditForm, CSVModelForm
|
||||
from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm
|
||||
|
||||
__all__ = (
|
||||
'CustomFieldModelCSVForm',
|
||||
@@ -34,6 +34,9 @@ class CustomFieldsMixin:
|
||||
raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(content_types=content_type)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field()
|
||||
|
||||
@@ -41,10 +44,7 @@ class CustomFieldsMixin:
|
||||
"""
|
||||
Append form fields for all CustomFields assigned to this object type.
|
||||
"""
|
||||
content_type = self._get_content_type()
|
||||
|
||||
# Append form fields; assign initial values if modifying and existing object
|
||||
for customfield in CustomField.objects.filter(content_types=content_type):
|
||||
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||
field_name = f'cf_{customfield.name}'
|
||||
self.fields[field_name] = self._get_form_field(customfield)
|
||||
|
||||
@@ -52,7 +52,7 @@ class CustomFieldsMixin:
|
||||
self.custom_fields.append(field_name)
|
||||
|
||||
|
||||
class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm):
|
||||
class CustomFieldModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
|
||||
"""
|
||||
Extend ModelForm to include custom field support.
|
||||
"""
|
||||
@@ -86,40 +86,37 @@ class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
||||
return customfield.to_form_field(for_csv_import=True)
|
||||
|
||||
|
||||
class CustomFieldModelBulkEditForm(BulkEditForm):
|
||||
class CustomFieldModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = CustomField.objects.filter(content_types=self.obj_type)
|
||||
for cf in custom_fields:
|
||||
def _append_customfield_fields(self):
|
||||
"""
|
||||
Append form fields for all CustomFields assigned to this object type.
|
||||
"""
|
||||
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||
# Annotate non-required custom fields as nullable
|
||||
if not cf.required:
|
||||
self.nullable_fields.append(cf.name)
|
||||
self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
|
||||
# Annotate this as a custom field
|
||||
self.custom_fields.append(cf.name)
|
||||
if not customfield.required:
|
||||
self.nullable_fields.append(customfield.name)
|
||||
|
||||
self.fields[customfield.name] = self._get_form_field(customfield)
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields.append(customfield.name)
|
||||
|
||||
|
||||
class CustomFieldModelFilterForm(forms.Form):
|
||||
class CustomFieldModelFilterForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
self.custom_field_filters = []
|
||||
custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||
)
|
||||
for cf in custom_fields:
|
||||
field_name = f'cf_{cf.name}'
|
||||
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
|
||||
self.custom_field_filters.append(field_name)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||
|
||||
@@ -9,9 +9,8 @@ from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, ContentTypeChoiceField,
|
||||
ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, StaticSelect,
|
||||
StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker,
|
||||
DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
|
||||
@@ -28,17 +27,12 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(BootstrapMixin, forms.Form):
|
||||
class CustomFieldFilterForm(FilterForm):
|
||||
field_groups = [
|
||||
['q'],
|
||||
['type', 'content_types'],
|
||||
['weight', 'required'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
@@ -61,19 +55,14 @@ class CustomFieldFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkFilterForm(BootstrapMixin, forms.Form):
|
||||
class CustomLinkFilterForm(FilterForm):
|
||||
field_groups = [
|
||||
['q'],
|
||||
['content_type', 'weight', 'new_window'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('custom_links'),
|
||||
required=False
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
@@ -87,19 +76,14 @@ class CustomLinkFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
|
||||
class ExportTemplateFilterForm(FilterForm):
|
||||
field_groups = [
|
||||
['q'],
|
||||
['content_type', 'mime_type', 'file_extension', 'as_attachment'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('export_templates'),
|
||||
required=False
|
||||
)
|
||||
mime_type = forms.CharField(
|
||||
@@ -117,20 +101,15 @@ class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterForm(BootstrapMixin, forms.Form):
|
||||
class WebhookFilterForm(FilterForm):
|
||||
field_groups = [
|
||||
['q'],
|
||||
['content_types', 'http_method', 'enabled'],
|
||||
['type_create', 'type_update', 'type_delete'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
required=False
|
||||
)
|
||||
http_method = forms.MultipleChoiceField(
|
||||
@@ -165,12 +144,8 @@ class WebhookFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
class TagFilterForm(FilterForm):
|
||||
model = Tag
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
required=False,
|
||||
@@ -178,7 +153,7 @@ class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
class ConfigContextFilterForm(FilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
@@ -186,77 +161,61 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
['cluster_group_id', 'cluster_id'],
|
||||
['tenant_group_id', 'tenant_id']
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Regions'),
|
||||
fetch_trigger='open'
|
||||
label=_('Regions')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site groups'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site groups')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label=_('Sites'),
|
||||
fetch_trigger='open'
|
||||
label=_('Sites')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
label=_('Device types'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device types')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Roles'),
|
||||
fetch_trigger='open'
|
||||
label=_('Roles')
|
||||
)
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
label=_('Platforms'),
|
||||
fetch_trigger='open'
|
||||
label=_('Platforms')
|
||||
)
|
||||
cluster_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Cluster groups'),
|
||||
fetch_trigger='open'
|
||||
label=_('Cluster groups')
|
||||
)
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label=_('Clusters'),
|
||||
fetch_trigger='open'
|
||||
label=_('Clusters')
|
||||
)
|
||||
tenant_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Tenant groups'),
|
||||
fetch_trigger='open'
|
||||
label=_('Tenant groups')
|
||||
)
|
||||
tenant_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
label=_('Tenant'),
|
||||
fetch_trigger='open'
|
||||
label=_('Tenant')
|
||||
)
|
||||
tag = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
label=_('Tags'),
|
||||
fetch_trigger='open'
|
||||
label=_('Tags')
|
||||
)
|
||||
|
||||
|
||||
@@ -270,18 +229,13 @@ class LocalConfigContextFilterForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
class JournalEntryFilterForm(FilterForm):
|
||||
model = JournalEntry
|
||||
field_groups = [
|
||||
['q'],
|
||||
['created_before', 'created_after', 'created_by_id'],
|
||||
['assigned_object_type_id', 'kind']
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
created_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
@@ -298,8 +252,7 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
@@ -307,8 +260,7 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
@@ -317,18 +269,13 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
class ObjectChangeFilterForm(FilterForm):
|
||||
model = ObjectChange
|
||||
field_groups = [
|
||||
['q'],
|
||||
['time_before', 'time_after', 'action'],
|
||||
['user_id', 'changed_object_type_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
@@ -350,8 +297,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
changed_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
@@ -359,6 +305,5 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/extras/content-types/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
|
||||
ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
|
||||
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
|
||||
@@ -41,6 +41,10 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
('Values', ('default', 'choices')),
|
||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||
)
|
||||
widgets = {
|
||||
'type': StaticSelect(),
|
||||
'filter_logic': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
@@ -57,6 +61,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
('Templates', ('link_text', 'link_url')),
|
||||
)
|
||||
widgets = {
|
||||
'button_class': StaticSelect(),
|
||||
'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
}
|
||||
@@ -70,14 +75,14 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
limit_choices_to=FeatureQuery('export_templates')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Custom Link', ('name', 'content_type', 'description')),
|
||||
('Export Template', ('name', 'content_type', 'description')),
|
||||
('Template', ('template_code',)),
|
||||
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
||||
)
|
||||
@@ -96,8 +101,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||
model = Webhook
|
||||
fields = '__all__'
|
||||
fieldsets = (
|
||||
('Webhook', ('name', 'enabled')),
|
||||
('Assigned Models', ('content_types',)),
|
||||
('Webhook', ('name', 'content_types', 'enabled')),
|
||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||
('HTTP Request', (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
@@ -105,7 +109,13 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||
('Conditions', ('conditions',)),
|
||||
('SSL', ('ssl_verification', 'ca_file_path')),
|
||||
)
|
||||
labels = {
|
||||
'type_create': 'Creations',
|
||||
'type_update': 'Updates',
|
||||
'type_delete': 'Deletions',
|
||||
}
|
||||
widgets = {
|
||||
'http_method': StaticSelect(),
|
||||
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ from django.utils import timezone
|
||||
from packaging import version
|
||||
|
||||
from extras.models import ObjectChange
|
||||
from netbox.config import Config
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Perform nightly housekeeping tasks. (This command can be run at any time.)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
config = Config()
|
||||
|
||||
# Clear expired authentication sessions (essentially replicating the `clearsessions` command)
|
||||
if options['verbosity']:
|
||||
@@ -37,10 +39,10 @@ class Command(BaseCommand):
|
||||
# Delete expired ObjectRecords
|
||||
if options['verbosity']:
|
||||
self.stdout.write("[*] Checking for expired changelog records")
|
||||
if settings.CHANGELOG_RETENTION:
|
||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||
if config.CHANGELOG_RETENTION:
|
||||
cutoff = timezone.now() - timedelta(days=config.CHANGELOG_RETENTION)
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(f"\tRetention period: {settings.CHANGELOG_RETENTION} days")
|
||||
self.stdout.write(f"\tRetention period: {config.CHANGELOG_RETENTION} days")
|
||||
self.stdout.write(f"\tCut-off time: {cutoff}")
|
||||
expired_records = ObjectChange.objects.filter(time__lt=cutoff).count()
|
||||
if expired_records:
|
||||
@@ -58,7 +60,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(
|
||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {settings.CHANGELOG_RETENTION})"
|
||||
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
|
||||
)
|
||||
|
||||
# Check for new releases (if enabled)
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization']
|
||||
APPS = ('circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0064_configrevision'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='imageattachment',
|
||||
name='last_updated',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
||||
18
netbox/extras/migrations/0066_customfield_name_validation.py
Normal file
18
netbox/extras/migrations/0066_customfield_name_validation.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0065_imageattachment_change_logging'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, unique=True, validators=[django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], message='Only alphanumeric characters and underscores are allowed.', regex='^[a-z0-9_]+$')]),
|
||||
),
|
||||
]
|
||||
21
netbox/extras/migrations/0067_customfield_min_max_values.py
Normal file
21
netbox/extras/migrations/0067_customfield_min_max_values.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0066_customfield_name_validation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='validation_maximum',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='validation_minimum',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -16,12 +16,19 @@ from extras.utils import FeatureQuery, extras_features
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities import filters
|
||||
from utilities.forms import (
|
||||
CSVChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
|
||||
CSVChoiceField, CSVMultipleChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect,
|
||||
add_blank_choice,
|
||||
)
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import validate_regex
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CustomField',
|
||||
'CustomFieldManager',
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
use_in_migrations = True
|
||||
|
||||
@@ -33,7 +40,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
return self.get_queryset().filter(content_types=content_type)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class CustomField(ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
@@ -49,7 +56,14 @@ class CustomField(ChangeLoggedModel):
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
help_text='Internal field name'
|
||||
help_text='Internal field name',
|
||||
validators=(
|
||||
RegexValidator(
|
||||
regex=r'^[a-z0-9_]+$',
|
||||
message="Only alphanumeric characters and underscores are allowed.",
|
||||
flags=re.IGNORECASE
|
||||
),
|
||||
)
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=50,
|
||||
@@ -83,13 +97,13 @@ class CustomField(ChangeLoggedModel):
|
||||
default=100,
|
||||
help_text='Fields with higher weights appear lower in a form.'
|
||||
)
|
||||
validation_minimum = models.PositiveIntegerField(
|
||||
validation_minimum = models.IntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Minimum value',
|
||||
help_text='Minimum allowed value (for numeric fields)'
|
||||
)
|
||||
validation_maximum = models.PositiveIntegerField(
|
||||
validation_maximum = models.IntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Maximum value',
|
||||
@@ -225,7 +239,7 @@ class CustomField(ChangeLoggedModel):
|
||||
"""
|
||||
Return a form field suitable for setting a CustomField's value for an object.
|
||||
|
||||
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
|
||||
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||
"""
|
||||
@@ -274,7 +288,7 @@ class CustomField(ChangeLoggedModel):
|
||||
choices=choices, required=required, initial=initial, widget=StaticSelect()
|
||||
)
|
||||
else:
|
||||
field_class = CSVChoiceField if for_csv_import else forms.MultipleChoiceField
|
||||
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
|
||||
field = field_class(
|
||||
choices=choices, required=required, initial=initial, widget=StaticSelectMultiple()
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class Webhook(ChangeLoggedModel):
|
||||
"""
|
||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||
@@ -125,8 +125,6 @@ class Webhook(ChangeLoggedModel):
|
||||
'Leave blank to use the system defaults.'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)
|
||||
@@ -179,7 +177,7 @@ class Webhook(ChangeLoggedModel):
|
||||
return json.dumps(context, cls=JSONEncoder)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class CustomLink(ChangeLoggedModel):
|
||||
"""
|
||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||
@@ -222,8 +220,6 @@ class CustomLink(ChangeLoggedModel):
|
||||
help_text="Force link to open in a new window"
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
|
||||
@@ -233,8 +229,26 @@ class CustomLink(ChangeLoggedModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:customlink', args=[self.pk])
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render the CustomLink given the provided context, and return the text, link, and link_target.
|
||||
|
||||
@extras_features('webhooks')
|
||||
:param context: The context passed to Jinja2
|
||||
"""
|
||||
text = render_jinja2(self.link_text, context)
|
||||
if not text:
|
||||
return {}
|
||||
link = render_jinja2(self.link_url, context)
|
||||
link_target = ' target="_blank"' if self.new_window else ''
|
||||
|
||||
return {
|
||||
'text': text,
|
||||
'link': link,
|
||||
'link_target': link_target,
|
||||
}
|
||||
|
||||
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class ExportTemplate(ChangeLoggedModel):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
@@ -268,8 +282,6 @@ class ExportTemplate(ChangeLoggedModel):
|
||||
help_text="Download file as attachment"
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['content_type', 'name']
|
||||
unique_together = [
|
||||
@@ -323,7 +335,8 @@ class ExportTemplate(ChangeLoggedModel):
|
||||
return response
|
||||
|
||||
|
||||
class ImageAttachment(BigIDModel):
|
||||
@extras_features('webhooks')
|
||||
class ImageAttachment(ChangeLoggedModel):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
"""
|
||||
@@ -347,12 +360,15 @@ class ImageAttachment(BigIDModel):
|
||||
max_length=50,
|
||||
blank=True
|
||||
)
|
||||
# ChangeLoggingMixin.created is a DateField
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = ('content_type', 'object_id')
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # name may be non-unique
|
||||
|
||||
@@ -394,6 +410,9 @@ class ImageAttachment(BigIDModel):
|
||||
except tuple(expected_exceptions):
|
||||
return None
|
||||
|
||||
def to_objectchange(self, action):
|
||||
return super().to_objectchange(action, related_object=self.parent)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class JournalEntry(ChangeLoggedModel):
|
||||
@@ -427,8 +446,6 @@ class JournalEntry(ChangeLoggedModel):
|
||||
)
|
||||
comments = models.TextField()
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
verbose_name_plural = 'journal entries'
|
||||
|
||||
@@ -7,14 +7,13 @@ from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class Tag(ChangeLoggedModel, TagBase):
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
@@ -24,8 +23,6 @@ class Tag(ChangeLoggedModel, TagBase):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
# Device type assignment is relevant only for Devices
|
||||
device_type = getattr(obj, 'device_type', None)
|
||||
|
||||
# Cluster assignment is relevant only for VirtualMachines
|
||||
# Get assigned Cluster and ClusterGroup, if any
|
||||
cluster = getattr(obj, 'cluster', None)
|
||||
cluster_group = getattr(cluster, 'group', None)
|
||||
|
||||
@@ -67,11 +67,8 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
Includes a method which appends an annotation of aggregated config context JSON data objects. This is
|
||||
implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
|
||||
This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
|
||||
multiple objects.
|
||||
|
||||
This allows the annotation to be entirely optional.
|
||||
multiple objects. This allows the annotation to be entirely optional.
|
||||
"""
|
||||
|
||||
def annotate_config_context_data(self):
|
||||
"""
|
||||
Attach the subquery annotation to the base queryset
|
||||
@@ -123,6 +120,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
elif self.model._meta.model_name == 'virtualmachine':
|
||||
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
|
||||
base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
|
||||
base_query.add(Q(device_types=None), Q.AND)
|
||||
region_field = 'cluster__site__region'
|
||||
sitegroup_field = 'cluster__site__group'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -20,7 +21,7 @@ from extras.models import JobResult
|
||||
from ipam.formfields import IPAddressFormField, IPNetworkFormField
|
||||
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from .context_managers import change_logging
|
||||
from .forms import ScriptForm
|
||||
|
||||
@@ -163,16 +164,22 @@ class ChoiceVar(ScriptVariable):
|
||||
def __init__(self, choices, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set field choices
|
||||
self.field_attrs['choices'] = choices
|
||||
# Set field choices, adding a blank choice to avoid forced selections
|
||||
self.field_attrs['choices'] = add_blank_choice(choices)
|
||||
|
||||
|
||||
class MultiChoiceVar(ChoiceVar):
|
||||
class MultiChoiceVar(ScriptVariable):
|
||||
"""
|
||||
Like ChoiceVar, but allows for the selection of multiple choices.
|
||||
"""
|
||||
form_field = forms.MultipleChoiceField
|
||||
|
||||
def __init__(self, choices, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set field choices
|
||||
self.field_attrs['choices'] = choices
|
||||
|
||||
|
||||
class ObjectVar(ScriptVariable):
|
||||
"""
|
||||
@@ -289,12 +296,21 @@ class BaseScript:
|
||||
|
||||
@classmethod
|
||||
def _get_vars(cls):
|
||||
vars = OrderedDict()
|
||||
vars = {}
|
||||
for name, attr in cls.__dict__.items():
|
||||
if name not in vars and issubclass(attr.__class__, ScriptVariable):
|
||||
vars[name] = attr
|
||||
|
||||
return vars
|
||||
# Order variables according to field_order
|
||||
field_order = getattr(cls.Meta, 'field_order', None)
|
||||
if not field_order:
|
||||
return vars
|
||||
ordered_vars = {
|
||||
field: vars.pop(field) for field in field_order if field in vars
|
||||
}
|
||||
ordered_vars.update(vars)
|
||||
|
||||
return ordered_vars
|
||||
|
||||
def run(self, data, commit):
|
||||
raise NotImplementedError("The script must define a run() method.")
|
||||
@@ -477,6 +493,10 @@ def get_scripts(use_names=False):
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
# Remove cached module to ensure consistency with filesystem
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
if use_names and hasattr(module, 'name'):
|
||||
module_name = module.name
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
from extras.validators import CustomValidator
|
||||
from netbox import thread_locals
|
||||
from netbox.config import get_config
|
||||
from netbox.request_context import get_request
|
||||
from netbox.signals import post_clean
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .models import ConfigRevision, CustomField, ObjectChange
|
||||
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
|
||||
|
||||
#
|
||||
# Change logging/webhooks
|
||||
#
|
||||
@@ -20,10 +23,16 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
clear_webhooks = Signal()
|
||||
|
||||
|
||||
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
def handle_changed_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
request = get_request()
|
||||
m2m_changed = False
|
||||
|
||||
def is_same_object(instance, webhook_data):
|
||||
return (
|
||||
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
|
||||
@@ -31,11 +40,6 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
request.id == webhook_data['request_id']
|
||||
)
|
||||
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
m2m_changed = False
|
||||
|
||||
# Determine the type of change being made
|
||||
if kwargs.get('created'):
|
||||
action = ObjectChangeActionChoices.ACTION_CREATE
|
||||
@@ -65,6 +69,7 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
objectchange.save()
|
||||
|
||||
# If this is an M2M change, update the previously queued webhook (from post_save)
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]):
|
||||
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
|
||||
webhook_queue[-1]['data'] = serialize_for_webhook(instance)
|
||||
@@ -79,13 +84,15 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
model_updates.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
def handle_deleted_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is deleted.
|
||||
"""
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
request = get_request()
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||
@@ -94,19 +101,21 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
objectchange.save()
|
||||
|
||||
# Enqueue webhooks
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
|
||||
# Increment metric counters
|
||||
model_deletes.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
|
||||
def clear_webhook_queue(sender, **kwargs):
|
||||
"""
|
||||
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
|
||||
"""
|
||||
logger = logging.getLogger('webhooks')
|
||||
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
|
||||
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
|
||||
webhook_queue.clear()
|
||||
|
||||
|
||||
@@ -157,9 +166,21 @@ m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_type
|
||||
|
||||
@receiver(post_clean)
|
||||
def run_custom_validators(sender, instance, **kwargs):
|
||||
config = get_config()
|
||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||
validators = settings.CUSTOM_VALIDATORS.get(model_name, [])
|
||||
validators = config.CUSTOM_VALIDATORS.get(model_name, [])
|
||||
|
||||
for validator in validators:
|
||||
|
||||
# Loading a validator class by dotted path
|
||||
if type(validator) is str:
|
||||
module, cls = validator.rsplit('.', 1)
|
||||
validator = getattr(importlib.import_module(module), cls)()
|
||||
|
||||
# Constructing a new instance on the fly from a ruleset
|
||||
elif type(validator) is dict:
|
||||
validator = CustomValidator(validator)
|
||||
|
||||
validator(instance)
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ CONFIGCONTEXT_ACTIONS = """
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if record.changed_object.get_absolute_url %}
|
||||
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% else %}
|
||||
{{ record.object_repr }}
|
||||
@@ -58,7 +58,7 @@ class CustomFieldTable(BaseTable):
|
||||
model = CustomField
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
|
||||
'description', 'filter_logic', 'choices',
|
||||
'description', 'filter_logic', 'choices', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
|
||||
|
||||
@@ -79,7 +79,7 @@ class CustomLinkTable(BaseTable):
|
||||
model = CustomLink
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window',
|
||||
'button_class', 'new_window', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
|
||||
|
||||
@@ -100,6 +100,7 @@ class ExportTemplateTable(BaseTable):
|
||||
model = ExportTemplate
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
@@ -134,7 +135,7 @@ class WebhookTable(BaseTable):
|
||||
model = Webhook
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||
'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
|
||||
'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||
@@ -156,7 +157,7 @@ class TagTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -193,7 +194,7 @@ class ConfigContextTable(BaseTable):
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
|
||||
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
||||
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user