mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-13 22:03:32 +01:00
Compare commits
385 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
869808b3f9 | ||
|
|
57ccbf44b8 | ||
|
|
416caa8f50 | ||
|
|
1e42fecf66 | ||
|
|
c9b00891ed | ||
|
|
497eacbea3 | ||
|
|
f90c591c78 | ||
|
|
175498940e | ||
|
|
eded00cbb3 | ||
|
|
e8d6281007 | ||
|
|
8299845615 | ||
|
|
9ae5865c2d | ||
|
|
c2d0cfdfc0 | ||
|
|
5dd252731e | ||
|
|
7b9436d2b9 | ||
|
|
6a369ac985 | ||
|
|
23d90823a3 | ||
|
|
4bfb6b476c | ||
|
|
0d60099588 | ||
|
|
94069e76c9 | ||
|
|
df9d67b873 | ||
|
|
f32e694499 | ||
|
|
e5900a3fe3 | ||
|
|
6e151b044d | ||
|
|
516bea6a0a | ||
|
|
496cabcc53 | ||
|
|
d051db5083 | ||
|
|
660fc23e15 | ||
|
|
a5a480133f | ||
|
|
68b544c676 | ||
|
|
a8c958ece2 | ||
|
|
f77f7ca0ec | ||
|
|
6b21c8453f | ||
|
|
fa8a8abc98 | ||
|
|
98cc36c458 | ||
|
|
f3beabba69 | ||
|
|
467fa5a847 | ||
|
|
50f283cf28 | ||
|
|
f49d7008a0 | ||
|
|
1fed564c47 | ||
|
|
bb99c3e6f9 | ||
|
|
8820cac792 | ||
|
|
ada911c20b | ||
|
|
17e01644f5 | ||
|
|
9458521f3e | ||
|
|
8aa73c5900 | ||
|
|
c0ca1eaf90 | ||
|
|
b29a5511df | ||
|
|
49e77841e0 | ||
|
|
daf6c8e327 | ||
|
|
9f8068e8d1 | ||
|
|
0b705553a5 | ||
|
|
a799094227 | ||
|
|
2f064cdfd1 | ||
|
|
6c28182dd3 | ||
|
|
3cb8c5db28 | ||
|
|
251abdb4dd | ||
|
|
726e4df54b | ||
|
|
bd32a6ac8e | ||
|
|
27d7400c36 | ||
|
|
53e52aeaa8 | ||
|
|
ae6ed97a80 | ||
|
|
3ad773beb3 | ||
|
|
be91235858 | ||
|
|
95fc0bbc94 | ||
|
|
9dad7e4daf | ||
|
|
d08ed9fe5f | ||
|
|
82210cc116 | ||
|
|
94d3e76517 | ||
|
|
3f72492a59 | ||
|
|
b7aa44837f | ||
|
|
7b7afd3e7b | ||
|
|
9c2514fce4 | ||
|
|
e04402ed57 | ||
|
|
3eda8d8482 | ||
|
|
79f2f03fb2 | ||
|
|
e5d7578663 | ||
|
|
773fd47ca6 | ||
|
|
830cf4b31f | ||
|
|
8f1acb700d | ||
|
|
7b1335825b | ||
|
|
11e2200acf | ||
|
|
f5356b84f6 | ||
|
|
1bf100ba15 | ||
|
|
7614f423e5 | ||
|
|
318c8b85e9 | ||
|
|
7085fe77da | ||
|
|
2e20d7f02b | ||
|
|
831065b5a1 | ||
|
|
b97167e841 | ||
|
|
19bacc9e23 | ||
|
|
61b61b1bc0 | ||
|
|
7c3318df92 | ||
|
|
d0b85586b9 | ||
|
|
cef0d168a5 | ||
|
|
3a192223a3 | ||
|
|
288a1d23e5 | ||
|
|
7c05db8e2f | ||
|
|
b7c0e8b71f | ||
|
|
a5ec0ee277 | ||
|
|
d528614cbf | ||
|
|
b5e8157700 | ||
|
|
24d6941cc4 | ||
|
|
0a62f75a40 | ||
|
|
a090955918 | ||
|
|
dfdeac4968 | ||
|
|
e84f2e3ad2 | ||
|
|
98ca4f5b5a | ||
|
|
87779b7b88 | ||
|
|
b56cae24c5 | ||
|
|
d48a68317d | ||
|
|
b07e88869a | ||
|
|
94bd27bcf5 | ||
|
|
090df05193 | ||
|
|
2c161c01c1 | ||
|
|
fc5a23cc88 | ||
|
|
73f2f9fc63 | ||
|
|
eb4b4a6c8d | ||
|
|
39430e01de | ||
|
|
96015aa590 | ||
|
|
c1720505f3 | ||
|
|
5c338a90a1 | ||
|
|
79cee12b1e | ||
|
|
aa5c42683a | ||
|
|
9c6938e7ae | ||
|
|
811c21ec7e | ||
|
|
84c14aadc7 | ||
|
|
f1f0d9cd0d | ||
|
|
e16942dea5 | ||
|
|
12efcec3b0 | ||
|
|
a7b6c40596 | ||
|
|
b95773938d | ||
|
|
6898ae7106 | ||
|
|
1a4f8c5422 | ||
|
|
66c4d23119 | ||
|
|
d66fc8f661 | ||
|
|
031876964f | ||
|
|
c63766c4c6 | ||
|
|
af6237e12e | ||
|
|
00328226ec | ||
|
|
b31ba4e9d2 | ||
|
|
4be5d3f9e9 | ||
|
|
53154746fc | ||
|
|
2f4c1b6e8f | ||
|
|
045ec7d3a0 | ||
|
|
b73db750e5 | ||
|
|
3f766ffea8 | ||
|
|
f28761202f | ||
|
|
6d1f07df05 | ||
|
|
eb9f2b36ab | ||
|
|
2bd29127dc | ||
|
|
3eef6363fd | ||
|
|
d451f30bfc | ||
|
|
105956f8e6 | ||
|
|
39256afb67 | ||
|
|
69aaf28b9c | ||
|
|
b806220074 | ||
|
|
d2bdf4e822 | ||
|
|
3ab5682e7a | ||
|
|
c0010ec100 | ||
|
|
6897c5fadd | ||
|
|
745aa23ed6 | ||
|
|
9089f5cf67 | ||
|
|
dd79aae137 | ||
|
|
26e470f521 | ||
|
|
a34c8b80e5 | ||
|
|
854a12982f | ||
|
|
cf173d4f50 | ||
|
|
7041486b93 | ||
|
|
548a8c3be3 | ||
|
|
087a018faf | ||
|
|
e09024e86f | ||
|
|
1757102536 | ||
|
|
c262af550d | ||
|
|
d9c6609b24 | ||
|
|
339bcb89bb | ||
|
|
b5884a5b54 | ||
|
|
c818d63043 | ||
|
|
c9c537a1b9 | ||
|
|
1be748b479 | ||
|
|
376c776520 | ||
|
|
a1f271d7d9 | ||
|
|
724997cb48 | ||
|
|
f3fe3f9a18 | ||
|
|
357a5d1e65 | ||
|
|
460e3fd5d6 | ||
|
|
257c0afdb5 | ||
|
|
ed3bc7cdcc | ||
|
|
bd181ac84f | ||
|
|
d1f5988db7 | ||
|
|
a5b99e7148 | ||
|
|
114500e7f4 | ||
|
|
d9f178e315 | ||
|
|
7337630704 | ||
|
|
0fdd081869 | ||
|
|
a9761e8dd2 | ||
|
|
1f1a05dc67 | ||
|
|
14b065cf5f | ||
|
|
47c3a20fda | ||
|
|
19c984bdab | ||
|
|
84d83fbd14 | ||
|
|
965ba3aca1 | ||
|
|
38f34ddb28 | ||
|
|
6b3e0d3229 | ||
|
|
854121b6ec | ||
|
|
047425dadd | ||
|
|
8dc0767cdf | ||
|
|
a99753fb0f | ||
|
|
ad65e06d0a | ||
|
|
bfb37d6283 | ||
|
|
fd180e480a | ||
|
|
3e5452d9da | ||
|
|
3ec0fe5519 | ||
|
|
71449b3414 | ||
|
|
16f5e233d0 | ||
|
|
abb72868f2 | ||
|
|
13e9d57d68 | ||
|
|
833acc3618 | ||
|
|
db522f96be | ||
|
|
df8b76127e | ||
|
|
8e849566d5 | ||
|
|
dba9602c75 | ||
|
|
9e2364b246 | ||
|
|
b5aecfeb91 | ||
|
|
c7523ffc67 | ||
|
|
d87d860a57 | ||
|
|
68b1234388 | ||
|
|
8fda08a1b5 | ||
|
|
aaba4b534f | ||
|
|
6d32aa8a88 | ||
|
|
0214c388ae | ||
|
|
e443d719d4 | ||
|
|
d514290688 | ||
|
|
694bd231e3 | ||
|
|
8523ad166e | ||
|
|
7ec6b4ebb7 | ||
|
|
38172b793b | ||
|
|
6bccb6d90b | ||
|
|
3cf1d6baf4 | ||
|
|
2a1718bfc8 | ||
|
|
0db4092266 | ||
|
|
41dfdc0aaa | ||
|
|
6bc72109c1 | ||
|
|
55e9685d30 | ||
|
|
d2dcc51430 | ||
|
|
214c1d5a50 | ||
|
|
38be0b4976 | ||
|
|
b86847c57e | ||
|
|
8ba5d03280 | ||
|
|
879ffd648b | ||
|
|
030c573037 | ||
|
|
713e79c1a9 | ||
|
|
383cdb5340 | ||
|
|
7b3f6f1c67 | ||
|
|
9cb29f48a0 | ||
|
|
5e29679968 | ||
|
|
84f3ab90df | ||
|
|
34db2eb611 | ||
|
|
16d8981a3f | ||
|
|
e67c965180 | ||
|
|
b0abfee35b | ||
|
|
1e8ee5e59b | ||
|
|
cc0830bf28 | ||
|
|
e3e005e327 | ||
|
|
42afd80e82 | ||
|
|
0b2862be54 | ||
|
|
8d703ffb36 | ||
|
|
6f24a938d9 | ||
|
|
574b57eadb | ||
|
|
aa05097fca | ||
|
|
de58f53f9f | ||
|
|
e738ff2fa7 | ||
|
|
25f501fb12 | ||
|
|
baf045aed6 | ||
|
|
e813dda275 | ||
|
|
ca131f12db | ||
|
|
ca72b07947 | ||
|
|
13d8957cf1 | ||
|
|
ca11b74c8e | ||
|
|
2ba6a6fc45 | ||
|
|
a6e79a1d61 | ||
|
|
1f4263aa6d | ||
|
|
147a4cbfb0 | ||
|
|
ab0a2abc54 | ||
|
|
57abbf1058 | ||
|
|
2a95e1bf71 | ||
|
|
bd957612c6 | ||
|
|
908e6a7a38 | ||
|
|
4493c31216 | ||
|
|
7a813349f3 | ||
|
|
ad7b8a9ac8 | ||
|
|
a226f06b1b | ||
|
|
b55c85b2af | ||
|
|
0d1d14bcd6 | ||
|
|
8c1a01d5ab | ||
|
|
cf8fdacfa3 | ||
|
|
2c1745ce28 | ||
|
|
950ce94653 | ||
|
|
851f8a1585 | ||
|
|
d40d1638af | ||
|
|
26ceeb61ef | ||
|
|
a39a9c9b56 | ||
|
|
45988b9818 | ||
|
|
7234b3bbf8 | ||
|
|
513ecd7e26 | ||
|
|
e12314ba60 | ||
|
|
9226302742 | ||
|
|
0e8c6ee522 | ||
|
|
a9c1c8968e | ||
|
|
6a15c2ae86 | ||
|
|
752de0d9c0 | ||
|
|
49617a595d | ||
|
|
2a293d5d02 | ||
|
|
9d99ede024 | ||
|
|
4a13ee6f40 | ||
|
|
2ba840c72c | ||
|
|
46cd55151d | ||
|
|
4d9691c8e5 | ||
|
|
f1687ef53d | ||
|
|
2fb55374b9 | ||
|
|
312246fec2 | ||
|
|
27c0e6dd5e | ||
|
|
0d7986e082 | ||
|
|
94300b221e | ||
|
|
a1110b07de | ||
|
|
a3069239e9 | ||
|
|
69f083428d | ||
|
|
113358f2de | ||
|
|
caa2813d0d | ||
|
|
481046c8b8 | ||
|
|
83f70dc28c | ||
|
|
8ede7a9297 | ||
|
|
ddff193786 | ||
|
|
774dff07ee | ||
|
|
4b14b31853 | ||
|
|
b0addfbe13 | ||
|
|
593874b45f | ||
|
|
b207f28402 | ||
|
|
7bdde47473 | ||
|
|
a2eb0d80d2 | ||
|
|
6f94198934 | ||
|
|
707e51d855 | ||
|
|
528df76747 | ||
|
|
662c896480 | ||
|
|
29eb2383d6 | ||
|
|
9772c5705f | ||
|
|
d2fe59ae8f | ||
|
|
d5e5cdda23 | ||
|
|
f63dcb1f08 | ||
|
|
6f66b27507 | ||
|
|
909d127c27 | ||
|
|
20ef18f98f | ||
|
|
a33e47780b | ||
|
|
691c66d2f5 | ||
|
|
14d87a3584 | ||
|
|
d743dc160a | ||
|
|
2b263b054c | ||
|
|
b95e8350d2 | ||
|
|
5235866d05 | ||
|
|
093a86bc38 | ||
|
|
5b87232f59 | ||
|
|
679bbd3e76 | ||
|
|
515b6bf71a | ||
|
|
9c389d9dcb | ||
|
|
f1e4273a23 | ||
|
|
4618cc2b22 | ||
|
|
1909f0c733 | ||
|
|
840ea36f70 | ||
|
|
a8cdb3895b | ||
|
|
349733c6dd | ||
|
|
1c09ffdd1f | ||
|
|
c4c6fa6042 | ||
|
|
86da6c6c14 | ||
|
|
7b7b01a26b | ||
|
|
415313ac2f | ||
|
|
7db2b9d091 | ||
|
|
8036d1e5a5 | ||
|
|
65c9339687 | ||
|
|
3090981335 | ||
|
|
4f36885c5e | ||
|
|
db2993035d | ||
|
|
bf05bc2986 | ||
|
|
88b230f0e4 | ||
|
|
deb53d771d | ||
|
|
b44ec35ade |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
||||
*.sh text eol=lf
|
||||
# Treat minified or packed JS/CSS files as binary, as they're not meant to be human-readable
|
||||
*.min.* binary
|
||||
*.map binary
|
||||
*.pack.js binary
|
||||
# Treat compiled JS/CSS files as binary, as they're not meant to be human-readable
|
||||
netbox/project-static/dist/*.css binary
|
||||
netbox/project-static/dist/*.js binary
|
||||
netbox/project-static/dist/*.js.map binary
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
13
.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.0
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.11
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -25,9 +22,9 @@ body:
|
||||
label: Python version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- 3.7
|
||||
- 3.8
|
||||
- 3.9
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
8
.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.0
|
||||
placeholder: v3.0.11
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -30,8 +30,10 @@ body:
|
||||
attributes:
|
||||
label: Proposed functionality
|
||||
description: >
|
||||
Describe in detail the new feature or behavior you'd like to propose. Include any specific
|
||||
changes to work flows, data models, or the user interface.
|
||||
Describe in detail the new feature or behavior you are proposing. Include any specific changes
|
||||
to work flows, data models, and/or the user interface. The more detail you provide here, the
|
||||
greater chance your proposal has of being discussed. Feature requests which don't include an
|
||||
actionable implementation plan will be rejected.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -58,6 +58,9 @@ jobs:
|
||||
|
||||
- name: Check UI ESLint, TypeScript, and Prettier Compliance
|
||||
run: yarn --cwd netbox/project-static validate
|
||||
|
||||
- name: Validate Static Asset Integrity
|
||||
run: scripts/verify-bundles.sh
|
||||
|
||||
- name: Run tests
|
||||
run: coverage run --source="netbox/" netbox/manage.py test netbox/
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,16 +1,13 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/netbox/project-static/.cache
|
||||
/netbox/project-static/node_modules
|
||||
/netbox/project-static/docs/*
|
||||
!/netbox/project-static/docs/.info
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/project-static/.cache
|
||||
/netbox/project-static/node_modules
|
||||
/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
|
||||
|
||||
@@ -94,10 +94,6 @@ Pillow
|
||||
# https://github.com/psycopg/psycopg2
|
||||
psycopg2-binary
|
||||
|
||||
# Extensive cryptographic library (fork of pycrypto)
|
||||
# https://github.com/Legrandin/pycryptodome
|
||||
pycryptodome
|
||||
|
||||
# YAML rendering library
|
||||
# https://github.com/yaml/pyyaml
|
||||
PyYAML
|
||||
|
||||
@@ -26,4 +26,4 @@ For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on you
|
||||
When deploying NetBox in a multiprocess manner (e.g. running multiple Gunicorn workers) the Prometheus client library requires the use of a shared directory to collect metrics from all worker processes. To configure this, first create or designate a local directory to which the worker processes have read and write access, and then configure your WSGI service (e.g. Gunicorn) to define this path as the `prometheus_multiproc_dir` environment variable.
|
||||
|
||||
!!! warning
|
||||
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
|
||||
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized environment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
|
||||
|
||||
@@ -1,85 +1,4 @@
|
||||
# Webhooks
|
||||
|
||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
|
||||
|
||||
!!! warning
|
||||
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
|
||||
|
||||
## Configuration
|
||||
|
||||
* **Name** - A unique name for the webhook. The name is not included with outbound messages.
|
||||
* **Object type(s)** - The type or types of NetBox object that will trigger the webhook.
|
||||
* **Enabled** - If unchecked, the webhook will be inactive.
|
||||
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
|
||||
* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
|
||||
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed.
|
||||
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
|
||||
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
||||
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
||||
* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
|
||||
* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!)
|
||||
* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional).
|
||||
|
||||
## Jinja2 Template Support
|
||||
|
||||
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
|
||||
|
||||
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
|
||||
|
||||
* Object type: IPAM > IP address
|
||||
* HTTP method: `POST`
|
||||
* URL: Slack incoming webhook URL
|
||||
* HTTP content type: `application/json`
|
||||
* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}`
|
||||
|
||||
### Available Context
|
||||
|
||||
The following data is available as context for Jinja2 templates:
|
||||
|
||||
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
|
||||
* `model` - The NetBox model which triggered the change.
|
||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||
* `username` - The name of the user account associated with the change.
|
||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "created",
|
||||
"timestamp": "2021-03-09 17:55:33.968016+00:00",
|
||||
"model": "site",
|
||||
"username": "jstretch",
|
||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
||||
"data": {
|
||||
"id": 19,
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status":
|
||||
"value": "active",
|
||||
"label": "Active",
|
||||
"id": 1
|
||||
},
|
||||
"region": null,
|
||||
...
|
||||
},
|
||||
"snapshots": {
|
||||
"prechange": null,
|
||||
"postchange": {
|
||||
"created": "2021-03-09",
|
||||
"last_updated": "2021-03-09T17:55:33.851Z",
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status": "active",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{!models/extras/webhook.md!}
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/optional-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 copied into your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
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.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside of scheduled execution times will not interfere with its operation.
|
||||
```shell
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
!!! note
|
||||
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
NetBox v2.9 introduced a new object-based permissions framework, which replace's Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
|
||||
{!docs/models/users/objectpermission.md!}
|
||||
{!models/users/objectpermission.md!}
|
||||
|
||||
### Example Constraint Definitions
|
||||
|
||||
|
||||
@@ -71,14 +71,3 @@ To extract the saved archive into a new installation, run the following from the
|
||||
```no-highlight
|
||||
tar -xf netbox_media.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Invalidation
|
||||
|
||||
If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache on the original instance by issuing the `invalidate all` management command (within the Python virtual environment):
|
||||
|
||||
```no-highlight
|
||||
# source /opt/netbox/venv/bin/activate
|
||||
(venv) # python3 manage.py invalidate all
|
||||
```
|
||||
|
||||
@@ -490,6 +490,14 @@ NetBox can be configured to support remote user authentication by inferring user
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SYNC_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to sync remote user groups by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER'`
|
||||
@@ -498,6 +506,54 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_GROUP'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSER_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Superuser on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_USERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SEPARATOR
|
||||
|
||||
Default: `|` (Pipe)
|
||||
|
||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## RELEASE_CHECK_URL
|
||||
|
||||
Default: None (disabled)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Circuits
|
||||
|
||||
{!docs/models/circuits/provider.md!}
|
||||
{!docs/models/circuits/providernetwork.md!}
|
||||
{!models/circuits/provider.md!}
|
||||
{!models/circuits/providernetwork.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/circuits/circuit.md!}
|
||||
{!docs/models/circuits/circuittype.md!}
|
||||
{!docs/models/circuits/circuittermination.md!}
|
||||
{!models/circuits/circuit.md!}
|
||||
{!models/circuits/circuittype.md!}
|
||||
{!models/circuits/circuittermination.md!}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Device Types
|
||||
|
||||
{!docs/models/dcim/devicetype.md!}
|
||||
{!docs/models/dcim/manufacturer.md!}
|
||||
{!models/dcim/devicetype.md!}
|
||||
{!models/dcim/manufacturer.md!}
|
||||
|
||||
---
|
||||
|
||||
@@ -30,11 +30,11 @@ Once component templates have been created, every new device that you create as
|
||||
!!! note
|
||||
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices.
|
||||
|
||||
{!docs/models/dcim/consoleporttemplate.md!}
|
||||
{!docs/models/dcim/consoleserverporttemplate.md!}
|
||||
{!docs/models/dcim/powerporttemplate.md!}
|
||||
{!docs/models/dcim/poweroutlettemplate.md!}
|
||||
{!docs/models/dcim/interfacetemplate.md!}
|
||||
{!docs/models/dcim/frontporttemplate.md!}
|
||||
{!docs/models/dcim/rearporttemplate.md!}
|
||||
{!docs/models/dcim/devicebaytemplate.md!}
|
||||
{!models/dcim/consoleporttemplate.md!}
|
||||
{!models/dcim/consoleserverporttemplate.md!}
|
||||
{!models/dcim/powerporttemplate.md!}
|
||||
{!models/dcim/poweroutlettemplate.md!}
|
||||
{!models/dcim/interfacetemplate.md!}
|
||||
{!models/dcim/frontporttemplate.md!}
|
||||
{!models/dcim/rearporttemplate.md!}
|
||||
{!models/dcim/devicebaytemplate.md!}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Devices and Cabling
|
||||
|
||||
{!docs/models/dcim/device.md!}
|
||||
{!docs/models/dcim/devicerole.md!}
|
||||
{!docs/models/dcim/platform.md!}
|
||||
{!models/dcim/device.md!}
|
||||
{!models/dcim/devicerole.md!}
|
||||
{!models/dcim/platform.md!}
|
||||
|
||||
---
|
||||
|
||||
@@ -10,20 +10,30 @@
|
||||
|
||||
Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources.
|
||||
|
||||
{!docs/models/dcim/consoleport.md!}
|
||||
{!docs/models/dcim/consoleserverport.md!}
|
||||
{!docs/models/dcim/powerport.md!}
|
||||
{!docs/models/dcim/poweroutlet.md!}
|
||||
{!docs/models/dcim/interface.md!}
|
||||
{!docs/models/dcim/frontport.md!}
|
||||
{!docs/models/dcim/rearport.md!}
|
||||
{!docs/models/dcim/devicebay.md!}
|
||||
{!docs/models/dcim/inventoryitem.md!}
|
||||
{!models/dcim/consoleport.md!}
|
||||
{!models/dcim/consoleserverport.md!}
|
||||
{!models/dcim/powerport.md!}
|
||||
{!models/dcim/poweroutlet.md!}
|
||||
{!models/dcim/interface.md!}
|
||||
{!models/dcim/frontport.md!}
|
||||
{!models/dcim/rearport.md!}
|
||||
{!models/dcim/devicebay.md!}
|
||||
{!models/dcim/inventoryitem.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/dcim/virtualchassis.md!}
|
||||
{!models/dcim/virtualchassis.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/dcim/cable.md!}
|
||||
{!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
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# IP Address Management
|
||||
|
||||
{!docs/models/ipam/aggregate.md!}
|
||||
{!docs/models/ipam/rir.md!}
|
||||
{!models/ipam/aggregate.md!}
|
||||
{!models/ipam/rir.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/ipam/prefix.md!}
|
||||
{!docs/models/ipam/role.md!}
|
||||
{!models/ipam/prefix.md!}
|
||||
{!models/ipam/role.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/ipam/iprange.md!}
|
||||
{!docs/models/ipam/ipaddress.md!}
|
||||
{!models/ipam/iprange.md!}
|
||||
{!models/ipam/ipaddress.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/ipam/vrf.md!}
|
||||
{!docs/models/ipam/routetarget.md!}
|
||||
{!models/ipam/vrf.md!}
|
||||
{!models/ipam/routetarget.md!}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Power Tracking
|
||||
|
||||
{!docs/models/dcim/powerpanel.md!}
|
||||
{!docs/models/dcim/powerfeed.md!}
|
||||
{!models/dcim/powerpanel.md!}
|
||||
{!models/dcim/powerfeed.md!}
|
||||
|
||||
# Example Power Topology
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Service Mapping
|
||||
|
||||
{!docs/models/ipam/service.md!}
|
||||
{!models/ipam/service.md!}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Sites and Racks
|
||||
|
||||
{!docs/models/dcim/region.md!}
|
||||
{!docs/models/dcim/sitegroup.md!}
|
||||
{!docs/models/dcim/site.md!}
|
||||
{!docs/models/dcim/location.md!}
|
||||
{!models/dcim/region.md!}
|
||||
{!models/dcim/sitegroup.md!}
|
||||
{!models/dcim/site.md!}
|
||||
{!models/dcim/location.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/dcim/rack.md!}
|
||||
{!docs/models/dcim/rackrole.md!}
|
||||
{!docs/models/dcim/rackreservation.md!}
|
||||
{!models/dcim/rack.md!}
|
||||
{!models/dcim/rackrole.md!}
|
||||
{!models/dcim/rackreservation.md!}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Tenancy Assignment
|
||||
|
||||
{!docs/models/tenancy/tenant.md!}
|
||||
{!docs/models/tenancy/tenantgroup.md!}
|
||||
{!models/tenancy/tenant.md!}
|
||||
{!models/tenancy/tenantgroup.md!}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Virtualization
|
||||
|
||||
{!docs/models/virtualization/cluster.md!}
|
||||
{!docs/models/virtualization/clustertype.md!}
|
||||
{!docs/models/virtualization/clustergroup.md!}
|
||||
{!models/virtualization/cluster.md!}
|
||||
{!models/virtualization/clustertype.md!}
|
||||
{!models/virtualization/clustergroup.md!}
|
||||
|
||||
---
|
||||
|
||||
{!docs/models/virtualization/virtualmachine.md!}
|
||||
{!docs/models/virtualization/vminterface.md!}
|
||||
{!models/virtualization/virtualmachine.md!}
|
||||
{!models/virtualization/vminterface.md!}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# VLAN Management
|
||||
|
||||
{!docs/models/ipam/vlan.md!}
|
||||
{!docs/models/ipam/vlangroup.md!}
|
||||
{!models/ipam/vlan.md!}
|
||||
{!models/ipam/vlangroup.md!}
|
||||
|
||||
@@ -1,44 +1,4 @@
|
||||
# Custom Fields
|
||||
|
||||
Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data.
|
||||
|
||||
Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects.
|
||||
|
||||
## Creating Custom Fields
|
||||
|
||||
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||
|
||||
* Text: Free-form text (up to 255 characters)
|
||||
* Integer: A whole number (positive or negative)
|
||||
* Boolean: True or false
|
||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||
* URL: This will be presented as a link in the web UI
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely.
|
||||
|
||||
A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields.
|
||||
|
||||
### Custom Field Validation
|
||||
|
||||
NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type:
|
||||
|
||||
* Text: Regular expression (optional)
|
||||
* Integer: Minimum and/or maximum value (optional)
|
||||
* Selection: Must exactly match one of the prescribed choices
|
||||
|
||||
### Custom Selection Fields
|
||||
|
||||
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
|
||||
|
||||
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
|
||||
{!models/extras/customfield.md!}
|
||||
|
||||
## Custom Fields in Templates
|
||||
|
||||
|
||||
@@ -45,6 +45,20 @@ Defining script variables is optional: You may create a script with only a `run(
|
||||
|
||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||
|
||||
By default, scripts within a module are ordered alphabetically in the scripts list page. To return scripts in a specific order, you can define the `script_order` variable at the end of your module. The `script_order` variable is a tuple which contains each Script class in the desired order. Any scripts that are omitted from this list will be listed last.
|
||||
|
||||
```python
|
||||
from extras.scripts import Script
|
||||
|
||||
class MyCustomScript(Script):
|
||||
...
|
||||
|
||||
class AnotherCustomScript(Script):
|
||||
...
|
||||
|
||||
script_order = (MyCustomScript, AnotherCustomScript)
|
||||
```
|
||||
|
||||
## Module Attributes
|
||||
|
||||
### `name`
|
||||
@@ -226,7 +240,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
||||
!!! note
|
||||
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
||||
|
||||

|
||||

|
||||
|
||||
### Via the Web UI
|
||||
|
||||
@@ -245,6 +259,22 @@ http://netbox/api/extras/scripts/example.MyReport/ \
|
||||
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
|
||||
```
|
||||
|
||||
### Via the CLI
|
||||
|
||||
Scripts can be run on the CLI by invoking the management command:
|
||||
|
||||
```
|
||||
python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>
|
||||
```
|
||||
|
||||
The required ``<module>.<script>`` argument is the script to run where ``<module>`` is the name of the python file in the ``scripts`` directory without the ``.py`` extension and ``<script>`` is the name of the script class in the ``<module>`` to run.
|
||||
|
||||
The optional ``--data "<data>"`` argument is the data to send to the script
|
||||
|
||||
The optional ``--loglevel`` argument is the desired logging level to output to the console.
|
||||
|
||||
The optional ``--commit`` argument will commit any changes in the script to the database.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an example script that creates new objects for a planned site. The user is prompted for three variables:
|
||||
|
||||
@@ -1,40 +1,4 @@
|
||||
# Export Templates
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates.
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
||||
|
||||
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
|
||||
|
||||
!!! note
|
||||
The name `table` is reserved for internal use.
|
||||
|
||||
!!! warning
|
||||
Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users.
|
||||
|
||||
The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
|
||||
|
||||
```jinja2
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
|
||||
```
|
||||
{% for server in queryset %}
|
||||
{% set data = server.get_config_context() %}
|
||||
{{ data.syslog }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
{!models/extras/exporttemplate.md!}
|
||||
|
||||
## REST API Integration
|
||||
|
||||
|
||||
@@ -97,6 +97,20 @@ The recording of one or more failure messages will automatically flag a report a
|
||||
|
||||
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.
|
||||
|
||||
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.
|
||||
|
||||
```
|
||||
from extras.reports import Report
|
||||
|
||||
class DeviceConnectionsReport(Report)
|
||||
pass
|
||||
|
||||
class DeviceIPsReport(Report)
|
||||
pass
|
||||
|
||||
report_order = (DeviceIPsReport, DeviceConnectionsReport)
|
||||
```
|
||||
|
||||
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
|
||||
|
||||
## Running Reports
|
||||
@@ -104,7 +118,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
|
||||
!!! note
|
||||
To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
|
||||
|
||||

|
||||

|
||||
|
||||
### Via the Web UI
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ class Foo(models.Model):
|
||||
|
||||
## 3. Update relevant querysets
|
||||
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retrieving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
|
||||
|
||||
## 4. Update API serializer
|
||||
|
||||
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 represenation of the model.
|
||||
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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ curl -H "Authorization: Token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
http://netbox/graphql/ \
|
||||
--data '{"query": "query {circuits(status:\"active\" {cid provider {name}}}"}'
|
||||
--data '{"query": "query {circuit_list(status:\"active\") {cid provider {name}}}"}'
|
||||
```
|
||||
|
||||
The response will include the requested data formatted as JSON:
|
||||
@@ -45,7 +45,7 @@ NetBox provides both a singular and plural query field for each object type:
|
||||
* `$OBJECT`: Returns a single object. Must specify the object's unique ID as `(id: 123)`.
|
||||
* `$OBJECT_list`: Returns a list of objects, optionally filtered by given parameters.
|
||||
|
||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of fitlers) to fetch all devices.
|
||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
||||
|
||||
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/).
|
||||
|
||||
@@ -54,7 +54,7 @@ For more detail on constructing GraphQL queries, see the [Graphene documentation
|
||||
The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active:
|
||||
|
||||
```
|
||||
{"query": "query {sites(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||
{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -10,7 +10,6 @@ NetBox is an infrastructure resource modeling (IRM) application designed to empo
|
||||
* **Connections** - Network, console, and power connections among devices
|
||||
* **Virtualization** - Virtual machines and clusters
|
||||
* **Data circuits** - Long-haul communications circuits and providers
|
||||
* **Secrets** - Encrypted storage of sensitive credentials
|
||||
|
||||
## What NetBox Is Not
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
sudo postgresql-setup --initdb
|
||||
```
|
||||
|
||||
!!! info
|
||||
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
|
||||
|
||||
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
|
||||
|
||||
```no-highlight
|
||||
|
||||
@@ -17,8 +17,13 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
||||
|
||||
=== "CentOS"
|
||||
|
||||
!!! warning
|
||||
CentOS 8 does not provide Python 3.7 or later via its native package manager. You will need to install it via some other means. [Here is an example](https://tecadmin.net/install-python-3-7-on-centos-8/) of installing Python 3.7 from source.
|
||||
|
||||
Once you have Python 3.7 or later installed, install the remaining system packages:
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
||||
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
||||
```
|
||||
|
||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||
@@ -70,19 +75,22 @@ If `git` is not already installed, install it:
|
||||
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
|
||||
|
||||
```no-highlight
|
||||
sudo git clone -b master https://github.com/netbox-community/netbox.git .
|
||||
sudo git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
|
||||
```
|
||||
|
||||
!!! note
|
||||
The `git clone` command above utilizes a "shallow clone" to retrieve only the most recent commit. If you need to download the entire history, omit the `--depth 1` argument.
|
||||
|
||||
The `git clone` command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
remote: Counting objects: 1994, done.
|
||||
remote: Compressing objects: 100% (150/150), done.
|
||||
remote: Total 1994 (delta 80), reused 0 (delta 0), pack-reused 1842
|
||||
Receiving objects: 100% (1994/1994), 472.36 KiB | 0 bytes/s, done.
|
||||
Resolving deltas: 100% (1495/1495), done.
|
||||
Checking connectivity... done.
|
||||
remote: Enumerating objects: 996, done.
|
||||
remote: Counting objects: 100% (996/996), done.
|
||||
remote: Compressing objects: 100% (935/935), done.
|
||||
remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0
|
||||
Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
|
||||
Resolving deltas: 100% (148/148), done.
|
||||
```
|
||||
|
||||
!!! note
|
||||
@@ -256,10 +264,10 @@ python3 manage.py createsuperuser
|
||||
|
||||
NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
|
||||
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to 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.)
|
||||
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
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
@@ -14,7 +14,7 @@ While the provided configuration should suffice for most initial installations,
|
||||
|
||||
## systemd Setup
|
||||
|
||||
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd dameon:
|
||||
We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon:
|
||||
|
||||
```no-highlight
|
||||
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.2. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
@@ -11,9 +11,9 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
5. [HTTP server](5-http-server.md)
|
||||
6. [LDAP authentication](6-ldap.md) (optional)
|
||||
|
||||
The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04 for your reference.
|
||||
The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/dFANGlxXEng" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -111,10 +111,10 @@ sudo systemctl restart netbox netbox-rq
|
||||
|
||||
## Verify Housekeeping Scheduling
|
||||
|
||||
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 copied to 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.)
|
||||
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
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
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
|
||||
|
||||
@@ -4,6 +4,6 @@ A platform defines the type of software running on a device or virtual machine.
|
||||
|
||||
Platforms may optionally be limited by manufacturer: If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
|
||||
|
||||
The platform model is also used to indicate which [NAPALM](../../additional-features/napalm.md) driver and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
|
||||
The platform model is also used to indicate which NAPALM driver (if any) and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
|
||||
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
41
docs/models/extras/customfield.md
Normal file
41
docs/models/extras/customfield.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Custom Fields
|
||||
|
||||
Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for _every_ NetBox installation. Instead, you can create a custom field to hold this data.
|
||||
|
||||
Within the database, custom fields are stored as JSON data directly alongside each object. This alleviates the need for complex queries when retrieving objects.
|
||||
|
||||
## Creating Custom Fields
|
||||
|
||||
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||
|
||||
* Text: Free-form text (up to 255 characters)
|
||||
* Integer: A whole number (positive or negative)
|
||||
* Boolean: True or false
|
||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||
* URL: This will be presented as a link in the web UI
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely.
|
||||
|
||||
A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields.
|
||||
|
||||
### Custom Field Validation
|
||||
|
||||
NetBox supports limited custom validation for custom field values. Following are the types of validation enforced for each field type:
|
||||
|
||||
* Text: Regular expression (optional)
|
||||
* Integer: Minimum and/or maximum value (optional)
|
||||
* Selection: Must exactly match one of the prescribed choices
|
||||
|
||||
### Custom Selection Fields
|
||||
|
||||
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
|
||||
|
||||
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
|
||||
@@ -1,8 +1,8 @@
|
||||
# Custom Links
|
||||
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
||||
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the Netbox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
|
||||
|
||||
For example, you might define a link like this:
|
||||
|
||||
37
docs/models/extras/exporttemplate.md
Normal file
37
docs/models/extras/exporttemplate.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Export Templates
|
||||
|
||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates.
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
||||
|
||||
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
|
||||
|
||||
!!! note
|
||||
The name `table` is reserved for internal use.
|
||||
|
||||
!!! warning
|
||||
Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users.
|
||||
|
||||
The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
|
||||
|
||||
```jinja2
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
|
||||
```
|
||||
{% for server in queryset %}
|
||||
{% set data = server.get_config_context() %}
|
||||
{{ data.syslog }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
82
docs/models/extras/webhook.md
Normal file
82
docs/models/extras/webhook.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Webhooks
|
||||
|
||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
|
||||
|
||||
!!! warning
|
||||
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
|
||||
|
||||
## Configuration
|
||||
|
||||
* **Name** - A unique name for the webhook. The name is not included with outbound messages.
|
||||
* **Object type(s)** - The type or types of NetBox object that will trigger the webhook.
|
||||
* **Enabled** - If unchecked, the webhook will be inactive.
|
||||
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
|
||||
* **HTTP method** - The type of HTTP request to send. Options include `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
|
||||
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed.
|
||||
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
|
||||
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
||||
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
||||
* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
|
||||
* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!)
|
||||
* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional).
|
||||
|
||||
## Jinja2 Template Support
|
||||
|
||||
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey object data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
|
||||
|
||||
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
|
||||
|
||||
* Object type: IPAM > IP address
|
||||
* HTTP method: `POST`
|
||||
* URL: Slack incoming webhook URL
|
||||
* HTTP content type: `application/json`
|
||||
* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}`
|
||||
|
||||
### Available Context
|
||||
|
||||
The following data is available as context for Jinja2 templates:
|
||||
|
||||
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
|
||||
* `model` - The NetBox model which triggered the change.
|
||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||
* `username` - The name of the user account associated with the change.
|
||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "created",
|
||||
"timestamp": "2021-03-09 17:55:33.968016+00:00",
|
||||
"model": "site",
|
||||
"username": "jstretch",
|
||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
||||
"data": {
|
||||
"id": 19,
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status":
|
||||
"value": "active",
|
||||
"label": "Active",
|
||||
"id": 1
|
||||
},
|
||||
"region": null,
|
||||
...
|
||||
},
|
||||
"snapshots": {
|
||||
"prechange": null,
|
||||
"postchange": {
|
||||
"created": "2021-03-09",
|
||||
"last_updated": "2021-03-09T17:55:33.851Z",
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status": "active",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -9,3 +9,6 @@ IP also ranges share the same functional roles as prefixes and VLANs, although t
|
||||
* Deprecated - No longer in use
|
||||
|
||||
The status of a range does _not_ have any impact on its member IP addresses, which may have their statuses modified separately.
|
||||
|
||||
!!! note
|
||||
The maximum supported size of an IP range is 2^32 - 1.
|
||||
|
||||
@@ -17,12 +17,12 @@ However, keep in mind that each piece of functionality is entirely optional. For
|
||||
|
||||
## Initial Setup
|
||||
|
||||
## Plugin Structure
|
||||
### Plugin Structure
|
||||
|
||||
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this:
|
||||
|
||||
```no-highlight
|
||||
plugin_name/
|
||||
project-name/
|
||||
- plugin_name/
|
||||
- templates/
|
||||
- plugin_name/
|
||||
@@ -38,13 +38,13 @@ plugin_name/
|
||||
- setup.py
|
||||
```
|
||||
|
||||
The top level is the project root. Immediately within the root should exist several items:
|
||||
The top level is the project root, which can have any name that you like. Immediately within the root should exist several items:
|
||||
|
||||
* `setup.py` - This is a standard installation script used to install the plugin package within the Python environment.
|
||||
* `README` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write README files using a markup language such as Markdown.
|
||||
* The plugin source directory, with the same name as your plugin.
|
||||
* The plugin source directory, with the same name as your plugin. This must be a valid Python package name (e.g. no spaces or hyphens).
|
||||
|
||||
The plugin source directory contains all of the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class.
|
||||
The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class.
|
||||
|
||||
### Create setup.py
|
||||
|
||||
@@ -118,6 +118,21 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
|
||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||
|
||||
### Create a Virtual Environment
|
||||
|
||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) specific to your plugin. This will afford you complete control over the installed versions of all dependencies and avoid conflicting with any system packages. This environment can live wherever you'd like, however it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
||||
|
||||
```shell
|
||||
python3 -m venv /path/to/my/venv
|
||||
```
|
||||
|
||||
You can make NetBox available within this environment by creating a path file pointing to its location. This will add NetBox to the Python path upon activation. (Be sure to adjust the command below to specify your actual virtual environment path, Python version, and NetBox installation.)
|
||||
|
||||
```shell
|
||||
cd $VENV/lib/python3.7/site-packages/
|
||||
echo /opt/netbox/netbox > netbox.pth
|
||||
```
|
||||
|
||||
### Install the Plugin for Development
|
||||
|
||||
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
|
||||
@@ -218,7 +233,7 @@ NetBox provides a base template to ensure a consistent user experience, which pl
|
||||
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
|
||||
|
||||
```jinja2
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base/layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
|
||||
|
||||
@@ -1,5 +1,276 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
## v3.0.9 (2021-11-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6529](https://github.com/netbox-community/netbox/issues/6529) - Introduce the `runscript` management command
|
||||
* [#6930](https://github.com/netbox-community/netbox/issues/6930) - Add an optional "ID" column to all tables
|
||||
* [#7668](https://github.com/netbox-community/netbox/issues/7668) - Add "view elevations" button to location view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7599](https://github.com/netbox-community/netbox/issues/7599) - Improve color mode preference handling
|
||||
* [#7601](https://github.com/netbox-community/netbox/issues/7601) - Correct devices count for locations within global search results
|
||||
* [#7612](https://github.com/netbox-community/netbox/issues/7612) - Strip HTML from custom field descriptions
|
||||
* [#7628](https://github.com/netbox-community/netbox/issues/7628) - Fix `load_yaml` method for custom scripts
|
||||
* [#7643](https://github.com/netbox-community/netbox/issues/7643) - Fix circuit assignment when creating multiple terminations simultaneously
|
||||
* [#7644](https://github.com/netbox-community/netbox/issues/7644) - Prevent inadvertent deletion of prior change records when deleting objects (#7333 revisited)
|
||||
* [#7647](https://github.com/netbox-community/netbox/issues/7647) - Require interface assignment when designating IP address as primary for device/VM during CSV import
|
||||
* [#7664](https://github.com/netbox-community/netbox/issues/7664) - Preserve initial form data when bulk edit validation fails
|
||||
* [#7717](https://github.com/netbox-community/netbox/issues/7717) - Restore missing tags column on IP range table
|
||||
* [#7721](https://github.com/netbox-community/netbox/issues/7721) - Retain pagination preference when `MAX_PAGE_SIZE` is zero
|
||||
|
||||
---
|
||||
|
||||
## v3.0.8 (2021-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
|
||||
* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
|
||||
* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
|
||||
* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
|
||||
* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
|
||||
* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
|
||||
* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
|
||||
* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
|
||||
* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
|
||||
* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
|
||||
|
||||
---
|
||||
|
||||
## v3.0.7 (2021-10-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6879](https://github.com/netbox-community/netbox/issues/6879) - Improve ability to toggle images/labels in rack elevations
|
||||
* [#7485](https://github.com/netbox-community/netbox/issues/7485) - Add USB micro AB type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7051](https://github.com/netbox-community/netbox/issues/7051) - Fix permissions evaluation and improve error handling for connected device REST API endpoint
|
||||
* [#7471](https://github.com/netbox-community/netbox/issues/7471) - Correct redirect URL when attaching images via "add another" button
|
||||
* [#7474](https://github.com/netbox-community/netbox/issues/7474) - Fix AttributeError exception when rendering a report or custom script
|
||||
* [#7479](https://github.com/netbox-community/netbox/issues/7479) - Fix parent interface choices when bulk editing VM interfaces
|
||||
|
||||
---
|
||||
|
||||
## v3.0.6 (2021-10-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6850](https://github.com/netbox-community/netbox/issues/6850) - Default to current user when creating journal entries via REST API
|
||||
* [#6955](https://github.com/netbox-community/netbox/issues/6955) - Include type, ID, and slug on object view
|
||||
* [#7394](https://github.com/netbox-community/netbox/issues/7394) - Enable filtering cables by termination type & ID in REST API
|
||||
* [#7462](https://github.com/netbox-community/netbox/issues/7462) - Include count of assigned virtual machines under platform view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7442](https://github.com/netbox-community/netbox/issues/7442) - Fix missing actions column on user-configured tables
|
||||
* [#7446](https://github.com/netbox-community/netbox/issues/7446) - Fix exception when viewing a large number of child IPs within a prefix
|
||||
* [#7455](https://github.com/netbox-community/netbox/issues/7455) - Fix site/provider network validation for circuit termination API serializer
|
||||
* [#7459](https://github.com/netbox-community/netbox/issues/7459) - Pre-populate location data when adding a device to a rack
|
||||
* [#7460](https://github.com/netbox-community/netbox/issues/7460) - Fix filtering connections by site ID
|
||||
|
||||
---
|
||||
|
||||
## v3.0.5 (2021-10-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5925](https://github.com/netbox-community/netbox/issues/5925) - Always show IP addresses tab under prefix view
|
||||
* [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications
|
||||
* [#6708](https://github.com/netbox-community/netbox/issues/6708) - Add image attachment support for circuits, power panels
|
||||
* [#7387](https://github.com/netbox-community/netbox/issues/7387) - Enable arbitrary ordering of custom scripts
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6433](https://github.com/netbox-community/netbox/issues/6433) - Fix bulk editing of child prefixes under aggregate view
|
||||
* [#6817](https://github.com/netbox-community/netbox/issues/6817) - Custom field columns should be removed from tables upon their deletion
|
||||
* [#6895](https://github.com/netbox-community/netbox/issues/6895) - Remove errant markup for null values in CSV export
|
||||
* [#7215](https://github.com/netbox-community/netbox/issues/7215) - Prevent rack elevations from overlapping when higher width is specified
|
||||
* [#7373](https://github.com/netbox-community/netbox/issues/7373) - Fix flashing when server, client, and browser color-mode preferences are mismatched
|
||||
* [#7397](https://github.com/netbox-community/netbox/issues/7397) - Fix AttributeError exception when rendering export template for devices via REST API
|
||||
* [#7401](https://github.com/netbox-community/netbox/issues/7401) - Pin `jsonschema` package to v3.2.0 to fix REST API docs rendering
|
||||
* [#7411](https://github.com/netbox-community/netbox/issues/7411) - Fix exception in UI when adding member devices to virtual chassis
|
||||
* [#7412](https://github.com/netbox-community/netbox/issues/7412) - Fix exception in UI when adding child device to device bay
|
||||
* [#7417](https://github.com/netbox-community/netbox/issues/7417) - Prevent exception when filtering objects list by invalid tag
|
||||
* [#7425](https://github.com/netbox-community/netbox/issues/7425) - Housekeeping command should honor zero verbosity
|
||||
* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
|
||||
|
||||
---
|
||||
|
||||
## v3.0.4 (2021-09-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6917](https://github.com/netbox-community/netbox/issues/6917) - Make IP assigned checkmark in IP table link to interface
|
||||
* [#6973](https://github.com/netbox-community/netbox/issues/6973) - Enable custom ordering of reports
|
||||
* [#7022](https://github.com/netbox-community/netbox/issues/7022) - Add ITA type C (CEE 7/16) power port type
|
||||
* [#7118](https://github.com/netbox-community/netbox/issues/7118) - Render URL custom fields as hyperlinks in object tables
|
||||
* [#7314](https://github.com/netbox-community/netbox/issues/7314) - Add SMA 905/906 fiber port types
|
||||
* [#7323](https://github.com/netbox-community/netbox/issues/7323) - Add serial filter field for racks & devices
|
||||
* [#7372](https://github.com/netbox-community/netbox/issues/7372) - Link to local docs for model from object add/edit views
|
||||
* [#7389](https://github.com/netbox-community/netbox/issues/7389) - Linkify tenant group in tenants list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7252](https://github.com/netbox-community/netbox/issues/7252) - Validate IP range size does not exceed max supported value
|
||||
* [#7294](https://github.com/netbox-community/netbox/issues/7294) - Fix SVG rendering for cable traces ending at unoccupied front ports
|
||||
* [#7304](https://github.com/netbox-community/netbox/issues/7304) - Require explicit values for all required choice fields during CSV import
|
||||
* [#7321](https://github.com/netbox-community/netbox/issues/7321) - Don't overwrite multi-select custom fields during bulk edit
|
||||
* [#7324](https://github.com/netbox-community/netbox/issues/7324) - Fix TypeError exception in web UI when filtering objects using single-choice filters
|
||||
* [#7333](https://github.com/netbox-community/netbox/issues/7333) - Prevent inadvertent deletion of prior change records when deleting objects
|
||||
* [#7341](https://github.com/netbox-community/netbox/issues/7341) - Fix incorrect URL in circuit breadcrumbs
|
||||
* [#7353](https://github.com/netbox-community/netbox/issues/7353) - Fix bulk creation of device/VM components via list view
|
||||
* [#7356](https://github.com/netbox-community/netbox/issues/7356) - Fix display of model documentation when adding device components
|
||||
* [#7358](https://github.com/netbox-community/netbox/issues/7358) - Add missing `choices` column to custom field CSV import form
|
||||
* [#7360](https://github.com/netbox-community/netbox/issues/7360) - Correct redirection URL after removing child device from device bay
|
||||
* [#7365](https://github.com/netbox-community/netbox/issues/7365) - Optimize performance when calculating prefix utilization
|
||||
* [#7374](https://github.com/netbox-community/netbox/issues/7374) - Add missing `face` parameter to API elevations request when editing device
|
||||
* [#7392](https://github.com/netbox-community/netbox/issues/7392) - Fix "help" links for custom fields, other models
|
||||
|
||||
---
|
||||
|
||||
## v3.0.3 (2021-09-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5775](https://github.com/netbox-community/netbox/issues/5775) - Enable synchronization of groups for remote authentication backend
|
||||
* [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type
|
||||
* [#6988](https://github.com/netbox-community/netbox/issues/6988) - Order tenants alphabetically without regard to group assignment
|
||||
* [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types
|
||||
* [#7087](https://github.com/netbox-community/netbox/issues/7087) - Add `local_context_data` filter for virtual machines list
|
||||
* [#7208](https://github.com/netbox-community/netbox/issues/7208) - Add navigation breadcrumbs for custom scripts & reports
|
||||
* [#7210](https://github.com/netbox-community/netbox/issues/7210) - Add search/filter forms for all organizational models
|
||||
* [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected
|
||||
* [#7284](https://github.com/netbox-community/netbox/issues/7284) - Include comments field in table/export for all appropriate models
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting
|
||||
* [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection
|
||||
* [#7228](https://github.com/netbox-community/netbox/issues/7228) - Improve temperature conversions under device status
|
||||
* [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links
|
||||
* [#7266](https://github.com/netbox-community/netbox/issues/7266) - Tweak font color for form field placeholder text
|
||||
* [#7273](https://github.com/netbox-community/netbox/issues/7273) - Fix natural ordering of device components in UI form fields
|
||||
* [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path
|
||||
* [#7282](https://github.com/netbox-community/netbox/issues/7282) - Fix KeyError exception when `INSECURE_SKIP_TLS_VERIFY` is true
|
||||
* [#7298](https://github.com/netbox-community/netbox/issues/7298) - Restore missing object names from applied object list filters
|
||||
* [#7301](https://github.com/netbox-community/netbox/issues/7301) - Fix exception when deleting a large number of child prefixes
|
||||
|
||||
---
|
||||
|
||||
## v3.0.2 (2021-09-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7131](https://github.com/netbox-community/netbox/issues/7131) - Fix issue where Site fields were hidden when editing a VLAN group
|
||||
* [#7148](https://github.com/netbox-community/netbox/issues/7148) - Fix issue where static query parameters with multiple values were not queried properly
|
||||
* [#7153](https://github.com/netbox-community/netbox/issues/7153) - Allow clearing of assigned device type images
|
||||
* [#7162](https://github.com/netbox-community/netbox/issues/7162) - Ensure consistent treatment of `BASE_PATH` for UI-driven API requests
|
||||
* [#7164](https://github.com/netbox-community/netbox/issues/7164) - Fix styling of "decommissioned" label for circuits
|
||||
* [#7169](https://github.com/netbox-community/netbox/issues/7169) - Fix CSV import file upload
|
||||
* [#7176](https://github.com/netbox-community/netbox/issues/7176) - Fix issue where query parameters were duplicated across different forms of the same type
|
||||
* [#7179](https://github.com/netbox-community/netbox/issues/7179) - Prevent obscuring "connect" pop-up for interfaces under device view
|
||||
* [#7188](https://github.com/netbox-community/netbox/issues/7188) - Fix issue where select fields with `null_option` did not render or send the null option
|
||||
* [#7189](https://github.com/netbox-community/netbox/issues/7189) - Set connection factory for django-redis when Sentinel is in use
|
||||
* [#7191](https://github.com/netbox-community/netbox/issues/7191) - Fix issue where API-backed multi-select elements cleared selected options when adding new options
|
||||
* [#7193](https://github.com/netbox-community/netbox/issues/7193) - Fix prefix (flat) template issue when viewing child prefixes with prefixes available
|
||||
* [#7205](https://github.com/netbox-community/netbox/issues/7205) - Fix issue where selected fields with `null_option` set were not added to applied filters
|
||||
* [#7209](https://github.com/netbox-community/netbox/issues/7209) - Allow unlimited API results when `MAX_PAGE_SIZE` is disabled
|
||||
|
||||
---
|
||||
|
||||
## v3.0.1 (2021-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7041](https://github.com/netbox-community/netbox/issues/7041) - Properly format JSON config object returned from a NAPALM device
|
||||
* [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI
|
||||
* [#7071](https://github.com/netbox-community/netbox/issues/7071) - Fix exception when removing a primary IP from a device/VM
|
||||
* [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views
|
||||
* [#7075](https://github.com/netbox-community/netbox/issues/7075) - Fix UI bug when a custom field has a space in the name
|
||||
* [#7080](https://github.com/netbox-community/netbox/issues/7080) - Fix missing image previews
|
||||
* [#7081](https://github.com/netbox-community/netbox/issues/7081) - Fix UI bug that did not properly request and handle paginated data
|
||||
* [#7082](https://github.com/netbox-community/netbox/issues/7082) - Avoid exception when referencing invalid content type in table
|
||||
* [#7083](https://github.com/netbox-community/netbox/issues/7083) - Correct labeling for VM memory attribute
|
||||
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix KeyError exception when editing access VLAN on an interface
|
||||
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix issue where hidden VLAN form fields were incorrectly included in the form submission
|
||||
* [#7089](https://github.com/netbox-community/netbox/issues/7089) - Fix filtering of change log by content type
|
||||
* [#7090](https://github.com/netbox-community/netbox/issues/7090) - Allow decimal input on length field when bulk editing cables
|
||||
* [#7091](https://github.com/netbox-community/netbox/issues/7091) - Ensure API requests from the UI are aware of `BASE_PATH`
|
||||
* [#7092](https://github.com/netbox-community/netbox/issues/7092) - Fix missing bulk edit buttons on Prefix IP Addresses table
|
||||
* [#7093](https://github.com/netbox-community/netbox/issues/7093) - Multi-select custom field filters should employ exact match
|
||||
* [#7096](https://github.com/netbox-community/netbox/issues/7096) - Home links should honor `BASE_PATH` configuration
|
||||
* [#7101](https://github.com/netbox-community/netbox/issues/7101) - Enforce `MAX_PAGE_SIZE` for table and REST API pagination
|
||||
* [#7106](https://github.com/netbox-community/netbox/issues/7106) - Fix incorrect "Map It" button URL on a site's physical address field
|
||||
* [#7107](https://github.com/netbox-community/netbox/issues/7107) - Fix missing search button and search results in IP address assignment "Assign IP" tab
|
||||
* [#7109](https://github.com/netbox-community/netbox/issues/7109) - Ensure human readability of exceptions raised during REST API requests
|
||||
* [#7113](https://github.com/netbox-community/netbox/issues/7113) - Show bulk edit/delete actions for prefix child objects
|
||||
* [#7123](https://github.com/netbox-community/netbox/issues/7123) - Remove "Global" placeholder for null VRF field
|
||||
* [#7124](https://github.com/netbox-community/netbox/issues/7124) - Fix duplicate static query param values in API Select
|
||||
|
||||
---
|
||||
|
||||
## v3.0.0 (2021-08-30)
|
||||
|
||||
!!! warning "Existing Deployments Must Upgrade from v2.11"
|
||||
@@ -178,7 +449,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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API.
|
||||
|
||||
{!docs/models/users/token.md!}
|
||||
{!models/users/token.md!}
|
||||
|
||||
## Authenticating to the API
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@ site_dir: netbox/project-static/docs
|
||||
site_url: https://netbox.readthedocs.io/
|
||||
repo_name: netbox-community/netbox
|
||||
repo_url: https://github.com/netbox-community/netbox
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
theme:
|
||||
name: material
|
||||
icon:
|
||||
@@ -24,13 +21,14 @@ extra:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/netbox-community/netbox
|
||||
- icon: fontawesome/brands/slack
|
||||
link: https://slack.netbox.dev
|
||||
link: https://netdev.chat/
|
||||
extra_css:
|
||||
- extra.css
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- markdown_include.include:
|
||||
base_path: 'docs/'
|
||||
headingOffset: 1
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
@@ -67,7 +65,7 @@ nav:
|
||||
- Customization:
|
||||
- Custom Fields: 'customization/custom-fields.md'
|
||||
- Custom Validation: 'customization/custom-validation.md'
|
||||
- Custom Links: 'customization/custom-links.md'
|
||||
- Custom Links: 'models/extras/customlink.md'
|
||||
- Export Templates: 'customization/export-templates.md'
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Reports: 'customization/reports.md'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'circuits.apps.CircuitsConfig'
|
||||
|
||||
@@ -3,10 +3,10 @@ from rest_framework import serializers
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
@@ -90,11 +90,11 @@ class CircuitSerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
|
||||
class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer(required=False)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -29,7 +29,7 @@ class CircuitStatusChoices(ChoiceSet):
|
||||
STATUS_PLANNED: 'info',
|
||||
STATUS_PROVISIONING: 'primary',
|
||||
STATUS_OFFLINE: 'danger',
|
||||
STATUS_DECOMMISSIONED: 'default',
|
||||
STATUS_DECOMMISSIONED: 'secondary',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
)
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, DatePicker,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SmallTextarea, SlugField,
|
||||
StaticSelect, StaticSelectMultiple, TagFilterField,
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .models import *
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
'admin_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
'asn': "BGP autonomous system number (if applicable)",
|
||||
'portal_url': "URL of the provider's customer support portal",
|
||||
'noc_contact': "NOC email address and phone number",
|
||||
'admin_contact': "Administrative contact email address and phone number",
|
||||
}
|
||||
|
||||
|
||||
class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = (
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label='Account number'
|
||||
)
|
||||
portal_url = forms.URLField(
|
||||
required=False,
|
||||
label='Portal'
|
||||
)
|
||||
noc_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='NOC contact'
|
||||
)
|
||||
admin_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='Admin contact'
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, 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'
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider Network', ('provider', 'name', 'description', 'tags')),
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, 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'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'description')
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
'install_date': DatePicker(),
|
||||
'commit_rate': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label='Commit rate (Kbps)'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Circuit
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['provider_id', 'provider_network_id'],
|
||||
['type_id', 'status', 'commit_rate'],
|
||||
['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'
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label=_('Commit rate (Kbps)')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
required=False
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
'upstream_speed': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)
|
||||
4
netbox/circuits/forms/__init__.py
Normal file
4
netbox/circuits/forms/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .filtersets import *
|
||||
from .models import *
|
||||
135
netbox/circuits/forms/bulk_edit.py
Normal file
135
netbox/circuits/forms/bulk_edit.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from django import forms
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderNetworkBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label='Account number'
|
||||
)
|
||||
portal_url = forms.URLField(
|
||||
required=False,
|
||||
label='Portal'
|
||||
)
|
||||
noc_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='NOC contact'
|
||||
)
|
||||
admin_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='Admin contact'
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect()
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label='Commit rate (Kbps)'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea,
|
||||
label='Comments'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
76
netbox/circuits/forms/bulk_import.py
Normal file
76
netbox/circuits/forms/bulk_import.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from extras.forms import CustomFieldModelCSVForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitCSVForm',
|
||||
'CircuitTypeCSVForm',
|
||||
'ProviderCSVForm',
|
||||
'ProviderNetworkCSVForm',
|
||||
)
|
||||
|
||||
|
||||
class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = (
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'description')
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(CustomFieldModelCSVForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned provider'
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
159
netbox/circuits/forms/filtersets.py
Normal file
159
netbox/circuits/forms/filtersets.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
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
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
'ProviderNetworkFilterForm',
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, 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'
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, 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'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
model = CircuitType
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Circuit
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['provider_id', 'provider_network_id'],
|
||||
['type_id', 'status', 'commit_rate'],
|
||||
['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'
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label=_('Commit rate (Kbps)')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
168
netbox/circuits/forms/models.py
Normal file
168
netbox/circuits/forms/models.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from django import forms
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
SelectSpeedWidget, SmallTextarea, SlugField, StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
'ProviderNetworkForm',
|
||||
)
|
||||
|
||||
|
||||
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asn', 'tags')),
|
||||
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
|
||||
)
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
'admin_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
'asn': "BGP autonomous system number (if applicable)",
|
||||
'portal_url': "URL of the provider's customer support portal",
|
||||
'noc_contact': "NOC email address and phone number",
|
||||
'admin_contact': "Administrative contact email address and phone number",
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'provider', 'name', 'description', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Provider Network', ('provider', 'name', 'description', 'tags')),
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
'install_date': DatePicker(),
|
||||
'commit_rate': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
required=False
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
'upstream_speed': SelectSpeedWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -202,6 +203,9 @@ class Circuit(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
# Cache associated CircuitTerminations
|
||||
termination_a = models.ForeignKey(
|
||||
|
||||
@@ -11,6 +11,7 @@ def update_circuit(instance, **kwargs):
|
||||
When a CircuitTermination has been modified, update its parent Circuit.
|
||||
"""
|
||||
termination_name = f'termination_{instance.term_side.lower()}'
|
||||
instance.circuit.refresh_from_db()
|
||||
setattr(instance.circuit, termination_name, instance)
|
||||
instance.circuit.save()
|
||||
|
||||
|
||||
@@ -2,10 +2,18 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CircuitTable',
|
||||
'CircuitTypeTable',
|
||||
'ProviderTable',
|
||||
'ProviderNetworkTable',
|
||||
)
|
||||
|
||||
|
||||
CIRCUITTERMINATION_LINK = """
|
||||
{% if value.site %}
|
||||
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
|
||||
@@ -28,6 +36,7 @@ class ProviderTable(BaseTable):
|
||||
accessor=Accessor('count_circuits'),
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:provider_list'
|
||||
)
|
||||
@@ -35,7 +44,8 @@ class ProviderTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags',
|
||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||
'comments', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
@@ -52,13 +62,14 @@ class ProviderNetworkTable(BaseTable):
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:providernetwork_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ProviderNetwork
|
||||
fields = ('pk', 'name', 'provider', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
@@ -78,7 +89,7 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
@@ -90,7 +101,7 @@ class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
verbose_name='Circuit ID'
|
||||
)
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
@@ -105,6 +116,7 @@ class CircuitTable(BaseTable):
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side Z'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
@@ -112,8 +124,8 @@ class CircuitTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'tags',
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
||||
@@ -136,14 +136,20 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
SIDE_A = CircuitTerminationSideChoices.SIDE_A
|
||||
SIDE_Z = CircuitTerminationSideChoices.SIDE_Z
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
|
||||
@@ -153,10 +159,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@@ -164,13 +170,13 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[1].pk,
|
||||
'site': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'site': sites[1].pk,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -122,10 +122,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider,type",
|
||||
"Circuit 4,Provider 1,Circuit Type 1",
|
||||
"Circuit 5,Provider 1,Circuit Type 1",
|
||||
"Circuit 6,Provider 1,Circuit Type 1",
|
||||
"cid,provider,type,status",
|
||||
"Circuit 4,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 5,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 6,Provider 1,Circuit Type 1,active",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
||||
@@ -34,9 +34,7 @@ class ProviderView(generic.ObjectView):
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'terminations__site'
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('provider')
|
||||
circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
@@ -97,10 +95,7 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'terminations__site'
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('termination_a')
|
||||
circuits_table.columns.hide('termination_z')
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
@@ -144,6 +139,8 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filtersets.CircuitTypeFilterSet
|
||||
filterset_form = forms.CircuitTypeFilterForm
|
||||
table = tables.CircuitTypeTable
|
||||
|
||||
|
||||
@@ -151,12 +148,8 @@ class CircuitTypeView(generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
type=instance
|
||||
)
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits)
|
||||
circuits_table.columns.hide('type')
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
|
||||
circuits_table = tables.CircuitTable(circuits, exclude=('type',))
|
||||
paginate_table(circuits_table, request)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'dcim.apps.DCIMConfig'
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -356,7 +356,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -538,7 +539,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerial
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -562,7 +563,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -585,7 +586,8 @@ class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
|
||||
@@ -2,7 +2,7 @@ import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseForbidden, HttpResponse
|
||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
@@ -17,12 +17,12 @@ from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.views import ModelViewSet
|
||||
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 utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related
|
||||
from utilities.utils import count_related, decode_dict
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
@@ -498,7 +498,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||
continue
|
||||
try:
|
||||
response[method] = getattr(d, method)()
|
||||
response[method] = decode_dict(getattr(d, method)())
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
@@ -675,15 +675,25 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
if not peer_device_name or not peer_interface_name:
|
||||
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
# Determine local endpoint from peer interface's connection
|
||||
peer_device = get_object_or_404(
|
||||
Device.objects.restrict(request.user, 'view'),
|
||||
name=peer_device_name
|
||||
)
|
||||
peer_interface = get_object_or_404(
|
||||
Interface.objects.all(),
|
||||
device__name=peer_device_name,
|
||||
Interface.objects.restrict(request.user, 'view'),
|
||||
device=peer_device,
|
||||
name=peer_interface_name
|
||||
)
|
||||
local_interface = peer_interface.connected_endpoint
|
||||
endpoint = peer_interface.connected_endpoint
|
||||
|
||||
if local_interface is None:
|
||||
return Response()
|
||||
# If an Interface, return the parent device
|
||||
if type(endpoint) is Interface:
|
||||
device = get_object_or_404(
|
||||
Device.objects.restrict(request.user, 'view'),
|
||||
pk=endpoint.device_id
|
||||
)
|
||||
return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
|
||||
|
||||
return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)
|
||||
# Connected endpoint is none or not an Interface
|
||||
raise Http404
|
||||
|
||||
@@ -185,6 +185,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'
|
||||
@@ -192,6 +193,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
@@ -201,6 +203,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'),
|
||||
@@ -210,6 +213,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_OTHER, 'Other'),
|
||||
@@ -316,6 +320,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_CS8365C = 'cs8365c'
|
||||
TYPE_CS8465C = 'cs8465c'
|
||||
# ITA/international
|
||||
TYPE_ITA_C = 'ita-c'
|
||||
TYPE_ITA_E = 'ita-e'
|
||||
TYPE_ITA_F = 'ita-f'
|
||||
TYPE_ITA_EF = 'ita-ef'
|
||||
@@ -336,6 +341,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_USB_3_B = 'usb-3-b'
|
||||
TYPE_USB_3_MICROB = 'usb-3-micro-b'
|
||||
# Direct current (DC)
|
||||
@@ -421,7 +427,8 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_CS8465C, 'CS8465C'),
|
||||
)),
|
||||
('International/ITA', (
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
|
||||
(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)'),
|
||||
@@ -442,6 +449,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
(TYPE_USB_3_B, 'USB 3.0 Type B'),
|
||||
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
|
||||
)),
|
||||
@@ -632,8 +640,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'),
|
||||
@@ -679,6 +687,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceKindChoices(ChoiceSet):
|
||||
KIND_PHYSICAL = 'physical'
|
||||
KIND_VIRTUAL = 'virtual'
|
||||
KIND_WIRELESS = 'wireless'
|
||||
|
||||
CHOICES = (
|
||||
(KIND_PHYSICAL, 'Physical'),
|
||||
(KIND_VIRTUAL, 'Virtual'),
|
||||
(KIND_WIRELESS, 'Wireless'),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Virtual
|
||||
@@ -719,6 +739,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'
|
||||
@@ -761,6 +782,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_T3 = 't3'
|
||||
TYPE_E3 = 'e3'
|
||||
|
||||
# ATM/DSL
|
||||
TYPE_XDSL = 'xdsl'
|
||||
|
||||
# Stacking
|
||||
TYPE_STACKWISE = 'cisco-stackwise'
|
||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||
@@ -827,6 +851,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)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -885,6 +910,12 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_E3, 'E3 (34 Mbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'ATM',
|
||||
(
|
||||
(TYPE_XDSL, 'xDSL'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Stacking',
|
||||
(
|
||||
@@ -958,6 +989,11 @@ class PortTypeChoices(ChoiceSet):
|
||||
TYPE_SPLICE = 'splice'
|
||||
TYPE_CS = 'cs'
|
||||
TYPE_SN = 'sn'
|
||||
TYPE_SMA_905 = 'sma-905'
|
||||
TYPE_SMA_906 = 'sma-906'
|
||||
TYPE_URM_P2 = 'urm-p2'
|
||||
TYPE_URM_P4 = 'urm-p4'
|
||||
TYPE_URM_P8 = 'urm-p8'
|
||||
|
||||
CHOICES = (
|
||||
(
|
||||
@@ -998,6 +1034,11 @@ class PortTypeChoices(ChoiceSet):
|
||||
(TYPE_ST, 'ST'),
|
||||
(TYPE_CS, 'CS'),
|
||||
(TYPE_SN, 'SN'),
|
||||
(TYPE_SMA_905, 'SMA 905'),
|
||||
(TYPE_SMA_906, 'SMA 906'),
|
||||
(TYPE_URM_P2, 'URM-P2'),
|
||||
(TYPE_URM_P4, 'URM-P4'),
|
||||
(TYPE_URM_P8, 'URM-P8'),
|
||||
(TYPE_SPLICE, 'Splice'),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,14 +10,14 @@ from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CableFilterSet',
|
||||
'CableTerminationFilterSet',
|
||||
@@ -480,12 +480,21 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
label='Device type (ID)',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
|
||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
@@ -852,6 +861,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):
|
||||
@@ -1184,6 +1204,10 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
termination_a_type = ContentTypeFilter()
|
||||
termination_a_id = MultiValueNumberFilter()
|
||||
termination_b_type = ContentTypeFilter()
|
||||
termination_b_id = MultiValueNumberFilter()
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CableTypeChoices
|
||||
)
|
||||
@@ -1228,7 +1252,7 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit']
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1243,73 +1267,6 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -1441,3 +1398,65 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
#
|
||||
# Connection filter sets
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site_id'
|
||||
)
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device_id'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
def filter_connections(self, queryset, name, value):
|
||||
if not value:
|
||||
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):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
5493
netbox/dcim/forms.py
5493
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
10
netbox/dcim/forms/__init__.py
Normal file
10
netbox/dcim/forms/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .fields import *
|
||||
from .models import *
|
||||
from .filtersets import *
|
||||
from .object_create import *
|
||||
from .object_import import *
|
||||
from .bulk_create import *
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .connections import *
|
||||
from .formsets import *
|
||||
111
netbox/dcim/forms/bulk_create.py
Normal file
111
netbox/dcim/forms/bulk_create.py
Normal file
@@ -0,0 +1,111 @@
|
||||
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 .object_create import ComponentForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortBulkCreateForm',
|
||||
'ConsoleServerPortBulkCreateForm',
|
||||
'DeviceBayBulkCreateForm',
|
||||
# 'FrontPortBulkCreateForm',
|
||||
'InterfaceBulkCreateForm',
|
||||
'InventoryItemBulkCreateForm',
|
||||
'PowerOutletBulkCreateForm',
|
||||
'PowerPortBulkCreateForm',
|
||||
'RearPortBulkCreateForm',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortBulkCreateForm(
|
||||
form_from_model(ConsolePort, ['type', 'speed', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsolePort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortBulkCreateForm(
|
||||
form_from_model(ConsoleServerPort, ['type', 'speed', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsoleServerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortBulkCreateForm(
|
||||
form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerOutletBulkCreateForm(
|
||||
form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerOutlet
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(
|
||||
form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = Interface
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
# class FrontPortBulkCreateForm(
|
||||
# form_from_model(FrontPort, ['label', 'type', 'description', 'tags']),
|
||||
# DeviceBulkAddComponentForm
|
||||
# ):
|
||||
# pass
|
||||
|
||||
|
||||
class RearPortBulkCreateForm(
|
||||
form_from_model(RearPort, ['type', 'color', 'positions', 'mark_connected']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = RearPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = DeviceBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemBulkCreateForm(
|
||||
form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = InventoryItem
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
'tags',
|
||||
)
|
||||
1090
netbox/dcim/forms/bulk_edit.py
Normal file
1090
netbox/dcim/forms/bulk_edit.py
Normal file
File diff suppressed because it is too large
Load Diff
970
netbox/dcim/forms/bulk_import.py
Normal file
970
netbox/dcim/forms/bulk_import.py
Normal file
@@ -0,0 +1,970 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelCSVForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||
from virtualization.models import Cluster
|
||||
|
||||
__all__ = (
|
||||
'CableCSVForm',
|
||||
'ChildDeviceCSVForm',
|
||||
'ConsolePortCSVForm',
|
||||
'ConsoleServerPortCSVForm',
|
||||
'DeviceBayCSVForm',
|
||||
'DeviceCSVForm',
|
||||
'DeviceRoleCSVForm',
|
||||
'FrontPortCSVForm',
|
||||
'InterfaceCSVForm',
|
||||
'InventoryItemCSVForm',
|
||||
'LocationCSVForm',
|
||||
'ManufacturerCSVForm',
|
||||
'PlatformCSVForm',
|
||||
'PowerFeedCSVForm',
|
||||
'PowerOutletCSVForm',
|
||||
'PowerPanelCSVForm',
|
||||
'PowerPortCSVForm',
|
||||
'RackCSVForm',
|
||||
'RackReservationCSVForm',
|
||||
'RackRoleCSVForm',
|
||||
'RearPortCSVForm',
|
||||
'RegionCSVForm',
|
||||
'SiteCSVForm',
|
||||
'SiteGroupCSVForm',
|
||||
'VirtualChassisCSVForm',
|
||||
)
|
||||
|
||||
|
||||
class RegionCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent region'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site group'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = ('name', 'slug', 'parent', 'description')
|
||||
|
||||
|
||||
class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
status = CSVChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
region = CSVModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned region'
|
||||
)
|
||||
group = CSVModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned group'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
)
|
||||
help_texts = {
|
||||
'time_zone': mark_safe(
|
||||
'Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent location',
|
||||
error_messages={
|
||||
'invalid_choice': 'Location not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('site', 'parent', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ('name', 'slug', 'color', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class RackCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = CSVModelChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned role'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=RackTypeChoices,
|
||||
required=False,
|
||||
help_text='Rack type'
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
choices=RackWidthChoices,
|
||||
help_text='Rail-to-rail width (in inches)'
|
||||
)
|
||||
outer_unit = CSVChoiceField(
|
||||
choices=RackDimensionUnitChoices,
|
||||
required=False,
|
||||
help_text='Unit for outer dimensions'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackReservationCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Parent site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Rack's location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Rack'
|
||||
)
|
||||
units = SimpleArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
required=True,
|
||||
help_text='Comma-separated list of individual unit numbers'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by assigned site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ('name', 'slug', 'description')
|
||||
|
||||
|
||||
class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ('name', 'slug', 'color', 'vm_role', 'description')
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
|
||||
class PlatformCSVForm(CustomFieldModelCSVForm):
|
||||
slug = SlugField()
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Limit platform assignments to this manufacturer'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description')
|
||||
|
||||
|
||||
class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||
device_role = CSVModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned role'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device type manufacturer'
|
||||
)
|
||||
device_type = CSVModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='model',
|
||||
help_text='Device type model'
|
||||
)
|
||||
platform = CSVModelChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned platform'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
virtual_chassis = CSVModelChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtual chassis'
|
||||
)
|
||||
cluster = CSVModelChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Virtualization cluster'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
model = Device
|
||||
help_texts = {
|
||||
'vc_position': 'Virtual chassis position',
|
||||
'vc_priority': 'Virtual chassis priority',
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit device type queryset by manufacturer
|
||||
params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')}
|
||||
self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params)
|
||||
|
||||
|
||||
class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Assigned location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Assigned rack (if any)"
|
||||
)
|
||||
face = CSVChoiceField(
|
||||
choices=DeviceFaceChoices,
|
||||
required=False,
|
||||
help_text='Mounted rack face'
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster',
|
||||
'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by assigned site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
|
||||
|
||||
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Parent device'
|
||||
)
|
||||
device_bay = CSVModelChoiceField(
|
||||
queryset=DeviceBay.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device bay in which this device is installed'
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit device bay queryset by parent device
|
||||
params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')}
|
||||
self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Set parent_bay reverse relationship
|
||||
device_bay = self.cleaned_data.get('device_bay')
|
||||
if device_bay:
|
||||
self.instance.parent_bay = device_bay
|
||||
|
||||
# Inherit site and rack from parent device
|
||||
parent = self.cleaned_data.get('parent')
|
||||
if parent:
|
||||
self.instance.site = parent.site
|
||||
self.instance.rack = parent.rack
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
speed = CSVTypedChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
coerce=int,
|
||||
empty_value=None,
|
||||
required=False,
|
||||
help_text='Port speed in bps'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
speed = CSVTypedChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
coerce=int,
|
||||
empty_value=None,
|
||||
required=False,
|
||||
help_text='Port speed in bps'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description')
|
||||
|
||||
|
||||
class PowerPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False,
|
||||
help_text='Port type'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False,
|
||||
help_text='Outlet type'
|
||||
)
|
||||
power_port = CSVModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Local power port which feeds this outlet'
|
||||
)
|
||||
feed_leg = CSVChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
required=False,
|
||||
help_text='Electrical phase (for three-phase circuits)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit PowerPort choices to those belonging to this device (or VC master)
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(
|
||||
device__in=[device, device.get_vc_master()]
|
||||
)
|
||||
else:
|
||||
self.fields['power_port'].queryset = PowerPort.objects.none()
|
||||
|
||||
|
||||
class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent interface'
|
||||
)
|
||||
lag = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Parent LAG interface'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
help_text='Physical medium'
|
||||
)
|
||||
mode = CSVChoiceField(
|
||||
choices=InterfaceModeChoices,
|
||||
required=False,
|
||||
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
|
||||
device = None
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
if device and device.virtual_chassis:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis)
|
||||
)
|
||||
elif device:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=device,
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['lag'].queryset = Interface.objects.none()
|
||||
self.fields['parent'].queryset = Interface.objects.none()
|
||||
|
||||
def clean_enabled(self):
|
||||
# Make sure enabled is True when it's not included in the uploaded data
|
||||
if 'enabled' not in self.data:
|
||||
return True
|
||||
else:
|
||||
return self.cleaned_data['enabled']
|
||||
|
||||
|
||||
class FrontPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
rear_port = CSVModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Corresponding rear port'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
help_text='Physical medium classification'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
)
|
||||
help_texts = {
|
||||
'rear_port_position': 'Mapped position on corresponding rear port',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit RearPort choices to those belonging to this device (or VC master)
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.filter(
|
||||
device__in=[device, device.get_vc_master()]
|
||||
)
|
||||
else:
|
||||
self.fields['rear_port'].queryset = RearPort.objects.none()
|
||||
|
||||
|
||||
class RearPortCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
help_text='Physical medium classification',
|
||||
choices=PortTypeChoices,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description')
|
||||
help_texts = {
|
||||
'positions': 'Number of front ports which may be mapped'
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
installed_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Child device installed within this bay',
|
||||
error_messages={
|
||||
'invalid_choice': 'Child device not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ('device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit installed device choices to devices of the correct type and location
|
||||
if self.is_bound:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
device = None
|
||||
else:
|
||||
try:
|
||||
device = self.instance.device
|
||||
except Device.DoesNotExist:
|
||||
device = None
|
||||
|
||||
if device:
|
||||
self.fields['installed_device'].queryset = Device.objects.filter(
|
||||
site=device.site,
|
||||
rack=device.rack,
|
||||
parent_bay__isnull=True,
|
||||
device_type__u_height=0,
|
||||
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||
).exclude(pk=device.pk)
|
||||
else:
|
||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
||||
|
||||
|
||||
class InventoryItemCSVForm(CustomFieldModelCSVForm):
|
||||
device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
manufacturer = CSVModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
parent = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Parent inventory item'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit parent choices to inventory items belonging to this device
|
||||
device = None
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
if device:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||
|
||||
|
||||
class CableCSVForm(CustomFieldModelCSVForm):
|
||||
# Termination A
|
||||
side_a_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Side A device'
|
||||
)
|
||||
side_a_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
||||
help_text='Side A type'
|
||||
)
|
||||
side_a_name = forms.CharField(
|
||||
help_text='Side A component name'
|
||||
)
|
||||
|
||||
# Termination B
|
||||
side_b_device = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Side B device'
|
||||
)
|
||||
side_b_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
||||
help_text='Side B type'
|
||||
)
|
||||
side_b_name = forms.CharField(
|
||||
help_text='Side B component name'
|
||||
)
|
||||
|
||||
# Cable attributes
|
||||
status = CSVChoiceField(
|
||||
choices=CableStatusChoices,
|
||||
required=False,
|
||||
help_text='Connection status'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=CableTypeChoices,
|
||||
required=False,
|
||||
help_text='Physical medium classification'
|
||||
)
|
||||
length_unit = CSVChoiceField(
|
||||
choices=CableLengthUnitChoices,
|
||||
required=False,
|
||||
help_text='Length unit'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
||||
'status', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
}
|
||||
|
||||
def _clean_side(self, side):
|
||||
"""
|
||||
Derive a Cable's A/B termination objects.
|
||||
|
||||
:param side: 'a' or 'b'
|
||||
"""
|
||||
assert side in 'ab', f"Invalid side designation: {side}"
|
||||
|
||||
device = self.cleaned_data.get(f'side_{side}_device')
|
||||
content_type = self.cleaned_data.get(f'side_{side}_type')
|
||||
name = self.cleaned_data.get(f'side_{side}_name')
|
||||
if not device or not content_type or not name:
|
||||
return None
|
||||
|
||||
model = content_type.model_class()
|
||||
try:
|
||||
termination_object = model.objects.get(device=device, name=name)
|
||||
if termination_object.cable is not None:
|
||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
||||
|
||||
setattr(self.instance, f'termination_{side}', termination_object)
|
||||
return termination_object
|
||||
|
||||
def clean_side_a_name(self):
|
||||
return self._clean_side('a')
|
||||
|
||||
def clean_side_b_name(self):
|
||||
return self._clean_side('b')
|
||||
|
||||
def clean_length_unit(self):
|
||||
# Avoid trying to save as NULL
|
||||
length_unit = self.cleaned_data.get('length_unit', None)
|
||||
return length_unit if length_unit is not None else ''
|
||||
|
||||
|
||||
class VirtualChassisCSVForm(CustomFieldModelCSVForm):
|
||||
master = CSVModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Master device'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ('name', 'domain', 'master')
|
||||
|
||||
|
||||
class PowerPanelCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ('site', 'location', 'name')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit group queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
|
||||
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
)
|
||||
power_panel = CSVModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Upstream power panel'
|
||||
)
|
||||
location = CSVModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text="Rack's location (if any)"
|
||||
)
|
||||
rack = CSVModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Rack'
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
help_text='Operational status'
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=PowerFeedTypeChoices,
|
||||
help_text='Primary or redundant'
|
||||
)
|
||||
supply = CSVChoiceField(
|
||||
choices=PowerFeedSupplyChoices,
|
||||
help_text='Supply type (AC/DC)'
|
||||
)
|
||||
phase = CSVChoiceField(
|
||||
choices=PowerFeedPhaseChoices,
|
||||
help_text='Single or three-phase'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
|
||||
'voltage', 'amperage', 'max_utilization', 'comments',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
|
||||
# Limit power_panel queryset by site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
|
||||
|
||||
# Limit location queryset by site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||
|
||||
# Limit rack queryset by site and group
|
||||
params = {
|
||||
f"site__{self.fields['site'].to_field_name}": data.get('site'),
|
||||
f"location__{self.fields['location'].to_field_name}": data.get('location'),
|
||||
}
|
||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||
49
netbox/dcim/forms/common.py
Normal file
49
netbox/dcim/forms/common.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
|
||||
__all__ = (
|
||||
'InterfaceCommonForm',
|
||||
)
|
||||
|
||||
|
||||
class InterfaceCommonForm(forms.Form):
|
||||
mac_address = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
label='MAC address'
|
||||
)
|
||||
mtu = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=INTERFACE_MTU_MIN,
|
||||
max_value=INTERFACE_MTU_MAX,
|
||||
label='MTU'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
|
||||
tagged_vlans = self.cleaned_data.get('tagged_vlans')
|
||||
|
||||
# Untagged interfaces cannot be assigned tagged VLANs
|
||||
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
|
||||
raise forms.ValidationError({
|
||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||
})
|
||||
|
||||
# Remove all tagged VLAN assignments from "tagged all" interfaces
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
|
||||
self.cleaned_data['tagged_vlans'] = []
|
||||
|
||||
# Validate tagged VLANs; must be a global VLAN or in the same site
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
|
||||
valid_sites = [None, self.cleaned_data[parent_field].site]
|
||||
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
|
||||
|
||||
if invalid_vlans:
|
||||
raise forms.ValidationError({
|
||||
'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
|
||||
f"the interface's parent device/VM, or they must be global"
|
||||
})
|
||||
287
netbox/dcim/forms/connections.py
Normal file
287
netbox/dcim/forms/connections.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from circuits.models import Circuit, CircuitTermination, Provider
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'ConnectCableToCircuitTerminationForm',
|
||||
'ConnectCableToConsolePortForm',
|
||||
'ConnectCableToConsoleServerPortForm',
|
||||
'ConnectCableToFrontPortForm',
|
||||
'ConnectCableToInterfaceForm',
|
||||
'ConnectCableToPowerFeedForm',
|
||||
'ConnectCableToPowerPortForm',
|
||||
'ConnectCableToPowerOutletForm',
|
||||
'ConnectCableToRearPortForm',
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
"""
|
||||
Base form for connecting a Cable to a Device component
|
||||
"""
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
label='Location',
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$termination_b_site'
|
||||
}
|
||||
)
|
||||
termination_b_rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
}
|
||||
)
|
||||
termination_b_device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
'rack_id': '$termination_b_rack',
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect,
|
||||
'type': StaticSelect,
|
||||
'length_unit': StaticSelect,
|
||||
}
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=ConsolePort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device',
|
||||
'kind': 'physical',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=FrontPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'device_id': '$termination_b_device'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
termination_b_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
required=False
|
||||
)
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_circuit = DynamicModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
label='Circuit',
|
||||
query_params={
|
||||
'provider_id': '$termination_b_provider',
|
||||
'site_id': '$termination_b_site',
|
||||
}
|
||||
)
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
label='Side',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'circuit_id': '$termination_b_circuit'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
)
|
||||
termination_b_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
label='Location',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site'
|
||||
}
|
||||
)
|
||||
termination_b_powerpanel = DynamicModelChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
label='Power Panel',
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$termination_b_site',
|
||||
'location_id': '$termination_b_location',
|
||||
}
|
||||
)
|
||||
termination_b_id = DynamicModelChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
label='Name',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'power_panel_id': '$termination_b_powerpanel'
|
||||
}
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
# Return the PK rather than the object
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
25
netbox/dcim/forms/fields.py
Normal file
25
netbox/dcim/forms/fields.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django import forms
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
__all__ = (
|
||||
'MACAddressField',
|
||||
)
|
||||
|
||||
|
||||
class MACAddressField(forms.Field):
|
||||
widget = forms.CharField
|
||||
default_error_messages = {
|
||||
'invalid': 'MAC address must be in EUI-48 format',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
|
||||
# Validate MAC address format
|
||||
try:
|
||||
value = EUI(value.strip())
|
||||
except AddrFormatError:
|
||||
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
||||
|
||||
return value
|
||||
1170
netbox/dcim/forms/filtersets.py
Normal file
1170
netbox/dcim/forms/filtersets.py
Normal file
File diff suppressed because it is too large
Load Diff
21
netbox/dcim/forms/formsets.py
Normal file
21
netbox/dcim/forms/formsets.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django import forms
|
||||
|
||||
__all__ = (
|
||||
'BaseVCMemberFormSet',
|
||||
)
|
||||
|
||||
|
||||
class BaseVCMemberFormSet(forms.BaseModelFormSet):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check for duplicate VC position values
|
||||
vc_position_list = []
|
||||
for form in self.forms:
|
||||
vc_position = form.cleaned_data.get('vc_position')
|
||||
if vc_position:
|
||||
if vc_position in vc_position_list:
|
||||
error_msg = f"A virtual chassis member already exists in position {vc_position}."
|
||||
form.add_error('vc_position', error_msg)
|
||||
vc_position_list.append(vc_position)
|
||||
1234
netbox/dcim/forms/models.py
Normal file
1234
netbox/dcim/forms/models.py
Normal file
File diff suppressed because it is too large
Load Diff
620
netbox/dcim/forms/object_create.py
Normal file
620
netbox/dcim/forms/object_create.py
Normal file
@@ -0,0 +1,620 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm, CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from ipam.models import VLAN
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
)
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortCreateForm',
|
||||
'ConsolePortTemplateCreateForm',
|
||||
'ConsoleServerPortCreateForm',
|
||||
'ConsoleServerPortTemplateCreateForm',
|
||||
'DeviceBayCreateForm',
|
||||
'DeviceBayTemplateCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InterfaceCreateForm',
|
||||
'InterfaceTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
'PowerPortCreateForm',
|
||||
'PowerPortTemplateCreateForm',
|
||||
'RearPortCreateForm',
|
||||
'RearPortTemplateCreateForm',
|
||||
'VirtualChassisCreateForm',
|
||||
)
|
||||
|
||||
|
||||
class ComponentForm(forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
a name pattern.
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
label_pattern = ExpandableNameField(
|
||||
label='Label',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of components being created from both the name_pattern and label_pattern are equal
|
||||
if self.cleaned_data['label_pattern']:
|
||||
name_pattern_count = len(self.cleaned_data['name_pattern'])
|
||||
label_pattern_count = len(self.cleaned_data['label_pattern'])
|
||||
if name_pattern_count != label_pattern_count:
|
||||
raise forms.ValidationError({
|
||||
'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however '
|
||||
f'{label_pattern_count} labels will be generated. These counts must match.'
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
members = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'rack_id': '$rack',
|
||||
}
|
||||
)
|
||||
initial_position = forms.IntegerField(
|
||||
initial=1,
|
||||
required=False,
|
||||
help_text='Position of the first member device. Increases by one for each additional member.'
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = [
|
||||
'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 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
|
||||
member.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
#
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
|
||||
"""
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'device_types': 'device_type'
|
||||
}
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum power draw (watts)"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
|
||||
'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to current DeviceType
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
||||
device_type=device_type
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only'
|
||||
)
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device_type = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device_type.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortCreateForm(ComponentCreateForm):
|
||||
model = ConsolePort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(ComponentCreateForm):
|
||||
model = ConsoleServerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
speed = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortSpeedChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortCreateForm(ComponentCreateForm):
|
||||
model = PowerPort
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Maximum draw in watts"
|
||||
)
|
||||
allocated_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
help_text="Allocated draw in watts"
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCreateForm(ComponentCreateForm):
|
||||
model = PowerOutlet
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
model = Interface
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
enabled = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
device_id = self.initial.get('device') or self.data.get('device')
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm):
|
||||
model = FrontPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
device = Device.objects.get(
|
||||
pk=self.initial.get('device') or self.data.get('device')
|
||||
)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||
# mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in device.frontports.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = RearPort.objects.filter(device=device)
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPortCreateForm(ComponentCreateForm):
|
||||
model = RearPort
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
widget=StaticSelect(),
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=REARPORT_POSITIONS_MIN,
|
||||
max_value=REARPORT_POSITIONS_MAX,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayCreateForm(ComponentCreateForm):
|
||||
model = DeviceBay
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
model = InventoryItem
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
}
|
||||
)
|
||||
part_id = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
label='Part ID'
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
)
|
||||
field_order = (
|
||||
'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag',
|
||||
'description', 'tags',
|
||||
)
|
||||
148
netbox/dcim/forms/object_import.py
Normal file
148
netbox/dcim/forms/object_import.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import InterfaceTypeChoices, PortTypeChoices
|
||||
from dcim.models import *
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
__all__ = (
|
||||
'ConsolePortTemplateImportForm',
|
||||
'ConsoleServerPortTemplateImportForm',
|
||||
'DeviceBayTemplateImportForm',
|
||||
'DeviceTypeImportForm',
|
||||
'FrontPortTemplateImportForm',
|
||||
'InterfaceTemplateImportForm',
|
||||
'PowerOutletTemplateImportForm',
|
||||
'PowerPortTemplateImportForm',
|
||||
'RearPortTemplateImportForm',
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Component template import forms
|
||||
#
|
||||
|
||||
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, device_type, data=None, *args, **kwargs):
|
||||
|
||||
# Must pass the parent DeviceType on form initialization
|
||||
data.update({
|
||||
'device_type': device_type.pk,
|
||||
})
|
||||
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
def clean_device_type(self):
|
||||
|
||||
data = self.cleaned_data['device_type']
|
||||
|
||||
# Limit fields referencing other components to the parent DeviceType
|
||||
for field_name, field in self.fields.items():
|
||||
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
|
||||
field.queryset = field.queryset.filter(device_type=data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices.CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
]
|
||||
|
||||
|
||||
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices.CHOICES
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypeChoices.CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'description',
|
||||
]
|
||||
@@ -185,11 +185,10 @@ class PathEndpoint(models.Model):
|
||||
|
||||
# Construct the complete path
|
||||
path = [self, *self._path.get_path()]
|
||||
if self._path.destination:
|
||||
path.append(self._path.destination)
|
||||
while len(path) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
|
||||
path.insert(-1, None)
|
||||
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)
|
||||
|
||||
# Return the path as a list of three-tuples (A termination, cable, B termination)
|
||||
return list(zip(*[iter(path)] * 3))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@@ -39,6 +40,9 @@ class PowerPanel(PrimaryModel):
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ class RackElevationSVG:
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
link.add(image)
|
||||
link.add(drawing.text(str(name), insert=text, stroke='black',
|
||||
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
|
||||
link.add(drawing.text(str(name), insert=text, fill='white', class_='device-image-label'))
|
||||
|
||||
def _draw_device_rear(self, drawing, device, start, end, text):
|
||||
rect = drawing.rect(start, end, class_="slot blocked")
|
||||
@@ -129,17 +132,24 @@ class RackElevationSVG:
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
drawing.add(image)
|
||||
drawing.add(drawing.text(str(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'))
|
||||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
link_url = '{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({
|
||||
'site': rack.site.pk,
|
||||
'location': rack.location.pk if rack.location else '',
|
||||
'rack': rack.pk,
|
||||
'face': face_id,
|
||||
'position': id_
|
||||
})
|
||||
)
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href='{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
|
||||
),
|
||||
target='_top'
|
||||
)
|
||||
drawing.a(href=link_url, target='_top')
|
||||
)
|
||||
if reservation:
|
||||
link.set_desc('{} — {} · {}'.format(
|
||||
@@ -432,15 +442,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 (either a Cable or attachment to a ProviderNetwork)
|
||||
if connector is not None:
|
||||
@@ -482,7 +493,7 @@ class CableTraceSVG:
|
||||
)
|
||||
parent_objects.append(parent_object)
|
||||
|
||||
else:
|
||||
elif far_end:
|
||||
|
||||
# Attachment
|
||||
attachment = self._draw_attachment()
|
||||
|
||||
@@ -43,6 +43,7 @@ class ConsoleConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
@@ -73,6 +74,7 @@ class PowerConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
@@ -106,3 +108,4 @@ class InterfaceConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Cable
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
|
||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
||||
|
||||
__all__ = (
|
||||
@@ -16,10 +16,6 @@ __all__ = (
|
||||
|
||||
class CableTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
id = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
)
|
||||
termination_a_parent = tables.TemplateColumn(
|
||||
template_code=CABLE_TERMINATION_PARENT,
|
||||
accessor=Accessor('termination_a'),
|
||||
@@ -45,7 +41,7 @@ class CableTable(BaseTable):
|
||||
verbose_name='Termination B'
|
||||
)
|
||||
status = ChoiceFieldColumn()
|
||||
length = tables.TemplateColumn(
|
||||
length = TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by='_abs_length'
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from dcim.models import (
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||
TagColumn, ToggleColumn,
|
||||
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
||||
)
|
||||
from .template_code import (
|
||||
CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS,
|
||||
@@ -18,6 +18,7 @@ from .template_code import (
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'BaseInterfaceTable',
|
||||
'ConsolePortTable',
|
||||
'ConsoleServerPortTable',
|
||||
'DeviceBayTable',
|
||||
@@ -52,6 +53,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.
|
||||
@@ -87,7 +96,7 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -115,7 +124,7 @@ class PlatformTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
fields = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'description', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -187,6 +196,7 @@ class DeviceTable(BaseTable):
|
||||
vc_priority = tables.Column(
|
||||
verbose_name='VC Priority'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:device_list'
|
||||
)
|
||||
@@ -194,9 +204,9 @@ class DeviceTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'tags',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||
@@ -225,7 +235,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
@@ -256,7 +266,7 @@ class CableTerminationTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='Cable Color'
|
||||
)
|
||||
cable_peer = tables.TemplateColumn(
|
||||
cable_peer = TemplateColumn(
|
||||
accessor='_cable_peer',
|
||||
template_code=CABLETERMINATION,
|
||||
orderable=False,
|
||||
@@ -266,7 +276,7 @@ class CableTerminationTable(BaseTable):
|
||||
|
||||
|
||||
class PathEndpointTable(CableTerminationTable):
|
||||
connection = tables.TemplateColumn(
|
||||
connection = TemplateColumn(
|
||||
accessor='_path.last_node',
|
||||
template_code=CABLETERMINATION,
|
||||
verbose_name='Connection',
|
||||
@@ -288,7 +298,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
@@ -309,7 +319,7 @@ class DeviceConsolePortTable(ConsolePortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions'
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
@@ -332,7 +342,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
@@ -354,7 +364,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
@@ -377,7 +387,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
@@ -399,7 +409,7 @@ class DevicePowerPortTable(PowerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -428,7 +438,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
@@ -449,7 +459,7 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -468,7 +478,7 @@ class BaseInterfaceTable(BaseTable):
|
||||
verbose_name='IP Addresses'
|
||||
)
|
||||
untagged_vlan = tables.Column(linkify=True)
|
||||
tagged_vlans = tables.TemplateColumn(
|
||||
tagged_vlans = TemplateColumn(
|
||||
template_code=INTERFACE_TAGGED_VLANS,
|
||||
orderable=False,
|
||||
verbose_name='Tagged VLANs'
|
||||
@@ -490,7 +500,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
@@ -499,8 +509,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
|
||||
class DeviceInterfaceTable(InterfaceTable):
|
||||
name = tables.TemplateColumn(
|
||||
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
|
||||
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
|
||||
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'),
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
@@ -522,7 +532,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'pk', 'id', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
@@ -532,7 +542,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,
|
||||
}
|
||||
@@ -559,7 +569,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -583,7 +593,7 @@ class DeviceFrontPortTable(FrontPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -610,7 +620,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||
@@ -632,7 +642,7 @@ class DeviceRearPortTable(RearPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -651,7 +661,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
|
||||
@@ -662,7 +673,7 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||
|
||||
|
||||
@@ -682,7 +693,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
|
||||
@@ -708,7 +719,7 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
@@ -729,7 +740,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -761,5 +772,5 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
@@ -5,7 +5,7 @@ from dcim.models import (
|
||||
Manufacturer, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
)
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, TagColumn, ToggleColumn,
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@@ -46,6 +46,9 @@ class ManufacturerTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
)
|
||||
|
||||
@@ -68,6 +71,7 @@ class DeviceTypeTable(BaseTable):
|
||||
url_params={'device_type_id': 'pk'},
|
||||
verbose_name='Instances'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:devicetype_list'
|
||||
)
|
||||
@@ -75,8 +79,8 @@ class DeviceTypeTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'instance_count', 'tags',
|
||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments', 'instance_count', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||
@@ -89,10 +93,16 @@ class DeviceTypeTable(BaseTable):
|
||||
|
||||
class ComponentTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
id = tables.Column(
|
||||
verbose_name='ID'
|
||||
)
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
@@ -101,7 +111,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_consoleports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -114,7 +124,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_consoleserverports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -127,7 +137,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_powerports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -140,7 +150,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_poweroutlets'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -156,7 +166,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_interfaces'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -173,7 +183,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_frontports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -187,7 +197,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_rearports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -200,7 +210,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_devicebays'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name', 'label', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn
|
||||
from .devices import CableTerminationTable
|
||||
|
||||
__all__ = (
|
||||
@@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class PowerFeedTable(CableTerminationTable):
|
||||
available_power = tables.Column(
|
||||
verbose_name='Available power (VA)'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:powerfeed_list'
|
||||
)
|
||||
@@ -69,9 +70,9 @@ class PowerFeedTable(CableTerminationTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power',
|
||||
'tags',
|
||||
'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
|
||||
@@ -4,13 +4,12 @@ from django_tables2.utils import Accessor
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn,
|
||||
ToggleColumn, UtilizationColumn,
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn,
|
||||
TagColumn, ToggleColumn, UtilizationColumn,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'RackTable',
|
||||
'RackDetailTable',
|
||||
'RackReservationTable',
|
||||
'RackRoleTable',
|
||||
)
|
||||
@@ -29,7 +28,7 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -56,17 +55,7 @@ class RackTable(BaseTable):
|
||||
template_code="{{ record.u_height }}U",
|
||||
verbose_name='Height'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
comments = MarkdownColumn()
|
||||
device_count = LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'rack_id': 'pk'},
|
||||
@@ -83,11 +72,20 @@ class RackDetailTable(RackTable):
|
||||
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(RackTable.Meta):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height', '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',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
@@ -125,7 +123,7 @@ class RackReservationTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -3,7 +3,7 @@ import django_tables2 as tables
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from .template_code import LOCATION_ELEVATIONS
|
||||
|
||||
@@ -33,7 +33,7 @@ class RegionTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class SiteGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ class SiteTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@@ -83,9 +84,9 @@ class SiteTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'tags',
|
||||
'contact_email', 'comments', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description')
|
||||
|
||||
@@ -119,5 +120,5 @@ class LocationTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Location
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions')
|
||||
|
||||
@@ -5,13 +5,11 @@ CABLETERMINATION = """
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CABLE_LENGTH = """
|
||||
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
|
||||
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% endif %}
|
||||
"""
|
||||
|
||||
CABLE_TERMINATION_PARENT = """
|
||||
@@ -42,17 +40,13 @@ 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>
|
||||
"""
|
||||
|
||||
@@ -63,8 +57,6 @@ INTERFACE_TAGGED_VLANS = """
|
||||
{% endfor %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
All
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@@ -583,6 +584,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'),
|
||||
@@ -594,14 +601,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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1032,14 +1042,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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1071,14 +1084,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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1138,6 +1154,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'),
|
||||
@@ -1149,14 +1171,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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1490,45 +1515,40 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype1 = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
)
|
||||
self.devicetype2 = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 2', slug='test-device-type-2'
|
||||
)
|
||||
self.devicerole1 = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
self.devicerole2 = DeviceRole.objects.create(
|
||||
name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
|
||||
)
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice1', site=self.site1
|
||||
device_type=devicetype, device_role=devicerole, name='TestDevice1', site=site
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice2', site=self.site1
|
||||
device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.interface3 = Interface.objects.create(device=self.device1, name='eth1') # Not connected
|
||||
|
||||
cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
||||
cable.save()
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_get_connected_device(self):
|
||||
|
||||
url = reverse('dcim-api:connected-device-list')
|
||||
response = self.client.get(url + '?peer_device=TestDevice2&peer_interface=eth0', **self.header)
|
||||
|
||||
url_params = f'?peer_device={self.device1.name}&peer_interface={self.interface1.name}'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['name'], self.device1.name)
|
||||
self.assertEqual(response.data['name'], self.device2.name)
|
||||
|
||||
url_params = f'?peer_device={self.device1.name}&peer_interface={self.interface3.name}'
|
||||
response = self.client.get(url + url_params, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@@ -2048,6 +2048,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'),
|
||||
@@ -2157,6 +2162,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]}
|
||||
@@ -2851,6 +2860,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
console_port = ConsolePort.objects.create(device=devices[0], name='Console Port 1')
|
||||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||
|
||||
# Cables
|
||||
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
@@ -2858,6 +2870,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['Cable 1', 'Cable 2']}
|
||||
@@ -2877,7 +2890,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [CableStatusChoices.STATUS_CONNECTED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'status': [CableStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -2888,30 +2901,44 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'device': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_site(self):
|
||||
site = Site.objects.all()[:2]
|
||||
params = {'site_id': [site[0].pk, site[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'site': [site[0].slug, site[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_tenant(self):
|
||||
tenant = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_termination_types(self):
|
||||
params = {'termination_a_type': 'dcim.consoleport'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'termination_b_type': 'dcim.consoleserverport'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_termination_ids(self):
|
||||
interface_ids = Cable.objects.values_list('termination_a_id', flat=True)[:3]
|
||||
params = {
|
||||
'termination_a_type': 'dcim.interface',
|
||||
'termination_a_id': list(interface_ids),
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@@ -322,10 +322,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,location,name,width,u_height",
|
||||
"Site 1,,Rack 4,19,42",
|
||||
"Site 1,Location 1,Rack 5,19,42",
|
||||
"Site 2,Location 2,Rack 6,19,42",
|
||||
"site,location,name,status,width,u_height",
|
||||
"Site 1,,Rack 4,active,19,42",
|
||||
"Site 1,Location 1,Rack 5,active,19,42",
|
||||
"Site 2,Location 2,Rack 6,active,19,42",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1991,10 +1991,10 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"site,power_panel,name,voltage,amperage,max_utilization",
|
||||
"Site 1,Power Panel 1,Power Feed 4,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 5,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 6,120,20,80",
|
||||
"site,power_panel,name,status,type,supply,phase,voltage,amperage,max_utilization",
|
||||
"Site 1,Power Panel 1,Power Feed 4,active,primary,ac,single-phase,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 5,active,primary,ac,single-phase,120,20,80",
|
||||
"Site 1,Power Panel 1,Power Feed 6,active,primary,ac,single-phase,120,20,80",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ImageAttachmentEditView, ObjectChangeLogView, ObjectJournalView
|
||||
from extras.views import ObjectChangeLogView, ObjectJournalView
|
||||
from ipam.views import ServiceEditView
|
||||
from utilities.views import SlugRedirectView
|
||||
from . import views
|
||||
@@ -43,7 +43,6 @@ urlpatterns = [
|
||||
path('sites/<int:pk>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
|
||||
path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Locations
|
||||
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
||||
@@ -55,7 +54,6 @@ urlpatterns = [
|
||||
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
||||
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
||||
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
|
||||
path('locations/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='location_add_image', kwargs={'model': Location}),
|
||||
|
||||
# Rack roles
|
||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
@@ -92,7 +90,6 @@ urlpatterns = [
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:pk>/journal/', ObjectJournalView.as_view(), name='rack_journal', kwargs={'model': Rack}),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
@@ -229,7 +226,6 @@ urlpatterns = [
|
||||
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'),
|
||||
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
# Console ports
|
||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch
|
||||
from django.db.models import Prefetch
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -131,8 +130,7 @@ class RegionView(generic.ObjectView):
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
region=instance
|
||||
)
|
||||
sites_table = tables.SiteTable(sites)
|
||||
sites_table.columns.hide('region')
|
||||
sites_table = tables.SiteTable(sites, exclude=('region',))
|
||||
paginate_table(sites_table, request)
|
||||
|
||||
return {
|
||||
@@ -216,8 +214,7 @@ class SiteGroupView(generic.ObjectView):
|
||||
sites = Site.objects.restrict(request.user, 'view').filter(
|
||||
group=instance
|
||||
)
|
||||
sites_table = tables.SiteTable(sites)
|
||||
sites_table.columns.hide('group')
|
||||
sites_table = tables.SiteTable(sites, exclude=('group',))
|
||||
paginate_table(sites_table, request)
|
||||
|
||||
return {
|
||||
@@ -440,6 +437,8 @@ class RackRoleListView(generic.ObjectListView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
filterset = filtersets.RackRoleFilterSet
|
||||
filterset_form = forms.RackRoleFilterForm
|
||||
table = tables.RackRoleTable
|
||||
|
||||
|
||||
@@ -451,8 +450,7 @@ class RackRoleView(generic.ObjectView):
|
||||
role=instance
|
||||
)
|
||||
|
||||
racks_table = tables.RackTable(racks)
|
||||
racks_table.columns.hide('role')
|
||||
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
|
||||
paginate_table(racks_table, request)
|
||||
|
||||
return {
|
||||
@@ -503,7 +501,7 @@ class RackListView(generic.ObjectListView):
|
||||
)
|
||||
filterset = filtersets.RackFilterSet
|
||||
filterset_form = forms.RackFilterForm
|
||||
table = tables.RackDetailTable
|
||||
table = tables.RackTable
|
||||
|
||||
|
||||
class RackElevationListView(generic.ObjectListView):
|
||||
@@ -684,6 +682,8 @@ class ManufacturerListView(generic.ObjectListView):
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
filterset = filtersets.ManufacturerFilterSet
|
||||
filterset_form = forms.ManufacturerFilterForm
|
||||
table = tables.ManufacturerTable
|
||||
|
||||
|
||||
@@ -700,8 +700,7 @@ class ManufacturerView(generic.ObjectView):
|
||||
manufacturer=instance
|
||||
)
|
||||
|
||||
devicetypes_table = tables.DeviceTypeTable(devicetypes)
|
||||
devicetypes_table.columns.hide('manufacturer')
|
||||
devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',))
|
||||
paginate_table(devicetypes_table, request)
|
||||
|
||||
return {
|
||||
@@ -1149,6 +1148,8 @@ class DeviceRoleListView(generic.ObjectListView):
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
vm_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
filterset = filtersets.DeviceRoleFilterSet
|
||||
filterset_form = forms.DeviceRoleFilterForm
|
||||
table = tables.DeviceRoleTable
|
||||
|
||||
|
||||
@@ -1159,9 +1160,7 @@ class DeviceRoleView(generic.ObjectView):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
device_role=instance
|
||||
)
|
||||
|
||||
devices_table = tables.DeviceTable(devices)
|
||||
devices_table.columns.hide('device_role')
|
||||
devices_table = tables.DeviceTable(devices, exclude=('device_role',))
|
||||
paginate_table(devices_table, request)
|
||||
|
||||
return {
|
||||
@@ -1225,13 +1224,12 @@ class PlatformView(generic.ObjectView):
|
||||
devices = Device.objects.restrict(request.user, 'view').filter(
|
||||
platform=instance
|
||||
)
|
||||
|
||||
devices_table = tables.DeviceTable(devices)
|
||||
devices_table.columns.hide('platform')
|
||||
devices_table = tables.DeviceTable(devices, exclude=('platform',))
|
||||
paginate_table(devices_table, request)
|
||||
|
||||
return {
|
||||
'devices_table': devices_table,
|
||||
'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count()
|
||||
}
|
||||
|
||||
|
||||
@@ -1872,9 +1870,9 @@ class InterfaceView(generic.ObjectView):
|
||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||
child_interfaces_tables = tables.InterfaceTable(
|
||||
child_interfaces,
|
||||
exclude=('device', 'parent'),
|
||||
orderable=False
|
||||
)
|
||||
child_interfaces_tables.columns.hide('device')
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
@@ -2171,9 +2169,10 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
|
||||
removed_device = device_bay.installed_device
|
||||
device_bay.installed_device = None
|
||||
device_bay.save()
|
||||
messages.success(request, "{} has been removed from {}.".format(removed_device, device_bay))
|
||||
messages.success(request, f"{removed_device} has been removed from {device_bay}.")
|
||||
return_url = self.get_return_url(request, device_bay.device)
|
||||
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
return redirect(return_url)
|
||||
|
||||
return render(request, 'dcim/devicebay_depopulate.html', {
|
||||
'device_bay': device_bay,
|
||||
@@ -2411,6 +2410,12 @@ class PathTraceView(generic.ObjectView):
|
||||
else:
|
||||
path = related_paths.first()
|
||||
|
||||
# No paths found
|
||||
if path is None:
|
||||
return {
|
||||
'path': None
|
||||
}
|
||||
|
||||
# Get the total length of the cable and whether the length is definitive (fully defined)
|
||||
total_length, is_definitive = path.get_total_length() if path else (None, False)
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'extras.apps.ExtrasConfig'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
@@ -30,6 +31,7 @@ __all__ = (
|
||||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
'JournalEntrySerializer',
|
||||
'ObjectChangeSerializer',
|
||||
'ReportDetailSerializer',
|
||||
'ReportSerializer',
|
||||
@@ -192,6 +194,12 @@ class JournalEntrySerializer(ValidatedModelSerializer):
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
created_by = serializers.PrimaryKeyRelatedField(
|
||||
allow_null=True,
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
kind = ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user