Compare commits
361 Commits
v3.0-beta2
...
v3.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fd16c47d2e | ||
|
|
b78451742f | ||
|
|
6f23ab5603 | ||
|
|
5e67627e6b | ||
|
|
19e77ed456 | ||
|
|
ed0f792f04 | ||
|
|
deda1691e9 | ||
|
|
94d2ad120c | ||
|
|
ab1a5f32ef | ||
|
|
2a1de5e28c | ||
|
|
f78fdd6900 | ||
|
|
6b43eafcb4 | ||
|
|
844cd154b9 | ||
|
|
e05fa5c302 | ||
|
|
f5f74944dd | ||
|
|
556efcc1d7 | ||
|
|
25d1fe2c8d | ||
|
|
b44ec35ade | ||
|
|
1a478150d6 | ||
|
|
e5643fb1e2 | ||
|
|
13e633778a | ||
|
|
bb57600f0f | ||
|
|
9813f3b696 | ||
|
|
3203db07b7 | ||
|
|
94b8d36065 | ||
|
|
0d61dcb1bc | ||
|
|
58203dbcfa | ||
|
|
66619cdc2f | ||
|
|
99cba25108 | ||
|
|
2fb1d388e3 | ||
|
|
6a4ed099fc | ||
|
|
d184ed4712 | ||
|
|
125a562189 | ||
|
|
a02ba5f7bb | ||
|
|
2e90f22529 | ||
|
|
bd681f5908 | ||
|
|
85b61c0b7e | ||
|
|
d11ea67bdd | ||
|
|
52603c087b | ||
|
|
545474a1a3 | ||
|
|
b63c838c74 | ||
|
|
1c6fdea27f | ||
|
|
9d0e6f0c30 | ||
|
|
1d0c72f5fa | ||
|
|
c221b9b4d4 | ||
|
|
a0ba8380c9 | ||
|
|
82a209bc5b | ||
|
|
2a338110f2 | ||
|
|
e890944160 | ||
|
|
542e01775e | ||
|
|
9cc4992fad | ||
|
|
6518d87200 | ||
|
|
499005f84d | ||
|
|
8497965cf7 | ||
|
|
0b0ab9277c | ||
|
|
75c62ff729 | ||
|
|
aef8c5fbb5 | ||
|
|
cfa4f5677b | ||
|
|
8131feae8a | ||
|
|
a3d5e04946 | ||
|
|
1fc3c6d9d2 | ||
|
|
53a5bc2221 | ||
|
|
12f3c2596f | ||
|
|
87dad41c37 | ||
|
|
4dbb18d408 | ||
|
|
a7cb75d73d | ||
|
|
517c0e2fe6 | ||
|
|
84db2e90ab | ||
|
|
9d469874c0 | ||
|
|
d850aa0773 | ||
|
|
9baebfa241 | ||
|
|
9e1d2da449 | ||
|
|
a71604e79f | ||
|
|
9a8d33e6bf | ||
|
|
09d745d987 | ||
|
|
8199bb6b62 | ||
|
|
643939ea1e | ||
|
|
9b3498d87a | ||
|
|
e4a162b054 | ||
|
|
bd47d0850e | ||
|
|
be3b4f0d3e | ||
|
|
664b02d735 | ||
|
|
6d1b981ecb | ||
|
|
ac6b1bf422 | ||
|
|
10847e2956 | ||
|
|
9b0258fef4 | ||
|
|
5b89cdc868 | ||
|
|
5a8cedd63f | ||
|
|
3feba2997f | ||
|
|
fce419526d | ||
|
|
ffae2c5f18 |
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
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -17,7 +17,7 @@ body:
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v2.11.11
|
||||
placeholder: v3.0.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -25,9 +25,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
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v2.11.11
|
||||
placeholder: v3.0.8
|
||||
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
|
||||
|
||||
15
.github/workflows/ci.yml
vendored
@@ -6,6 +6,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
node-version: [14.x]
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -33,12 +34,18 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies & set up configuration
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage
|
||||
ln -s configuration.testing.py netbox/netbox/configuration.py
|
||||
yarn --cwd netbox/project-static
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
@@ -47,7 +54,13 @@ jobs:
|
||||
run: python netbox/manage.py collectstatic --no-input
|
||||
|
||||
- name: Check PEP8 compliance
|
||||
run: pycodestyle --ignore=W504,E501 netbox/
|
||||
run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
|
||||
|
||||
- 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
@@ -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/*
|
||||
|
||||
10
README.md
@@ -54,13 +54,15 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
")
|
||||
|
||||

|
||||
")
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
### Related projects
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NAPALM
|
||||
|
||||
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
|
||||
NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
|
||||
|
||||
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
```
|
||||
|
||||
@@ -273,6 +273,16 @@ LOGGING = {
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_PERSISTENCE
|
||||
|
||||
Default: False
|
||||
|
||||
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
|
||||
|
||||
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_REQUIRED
|
||||
|
||||
Default: False
|
||||
@@ -333,7 +343,7 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
|
||||
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional.
|
||||
|
||||
!!! note
|
||||
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
|
||||
@@ -480,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'`
|
||||
@@ -488,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,20 @@
|
||||
|
||||
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!}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
99
docs/development/web-ui.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Web UI Development
|
||||
|
||||
## Front End Technologies
|
||||
|
||||
The NetBox UI is built on languages and frameworks:
|
||||
|
||||
### Styling & HTML Elements
|
||||
|
||||
#### [Bootstrap](https://getbootstrap.com/) 5
|
||||
|
||||
The majority of the NetBox UI is made up of stock Bootstrap components, with some styling modifications and custom components added on an as-needed basis. Bootstrap uses [Sass](https://sass-lang.com/), and NetBox extends Bootstrap's core Sass files for theming and customization.
|
||||
|
||||
### Client-side Scripting
|
||||
|
||||
#### [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
All client-side scripting is transpiled from TypeScript to JavaScript and served by Django. In development, TypeScript is an _extremely_ effective tool for accurately describing and checking the code, which leads to significantly fewer bugs, a better development experience, and more predictable/readable code.
|
||||
|
||||
As part of the [bundling](#bundling) process, Bootstrap's JavaScript plugins are imported and bundled alongside NetBox's front-end code.
|
||||
|
||||
!!! danger "NetBox is jQuery-free"
|
||||
Following the Bootstrap team's deprecation of jQuery in Bootstrap 5, NetBox also no longer uses jQuery in front-end code.
|
||||
|
||||
## Guidance
|
||||
|
||||
NetBox generally follows the following guidelines for front-end code:
|
||||
|
||||
- Bootstrap utility classes may be used to solve one-off issues or to implement singular components, as long as the class list does not exceed 4-5 classes. If an element needs more than 5 utility classes, a custom SCSS class should be added that contains the required style properties.
|
||||
- Custom classes must be commented, explaining the general purpose of the class and where it is used.
|
||||
- Reuse SCSS variables whenever possible. CSS values should (almost) never be hard-coded.
|
||||
- All TypeScript functions must have, at a minimum, a basic [JSDoc](https://jsdoc.app/) description of what the function is for and where it is used. If possible, document all function arguments via [`@param` JSDoc block tags](https://jsdoc.app/tags-param.html).
|
||||
- Expanding on NetBox's [dependency policy](style-guide.md#introducing-new-dependencies), new front-end dependencies should be avoided unless absolutely necessary. Every new front-end dependency adds to the CSS/JavaScript file size that must be loaded by the client and this should be minimized as much as possible. If adding a new dependency is unavoidable, use a tool like [Bundlephobia](https://bundlephobia.com/) to ensure the smallest possible library is used.
|
||||
- All UI elements must be usable on all common screen sizes, including mobile devices. Be sure to test newly implemented solutions (JavaScript included) on as many screen sizes and device types as possible.
|
||||
- NetBox aligns with Bootstrap's [supported Browsers and Devices](https://getbootstrap.com/docs/5.1/getting-started/browsers-devices/) list.
|
||||
|
||||
## UI Development
|
||||
|
||||
To contribute to the NetBox UI, you'll need to review the main [Getting Started guide](getting-started.md) in order to set up your base environment.
|
||||
|
||||
### Tools
|
||||
|
||||
Once you have a working NetBox development environment, you'll need to install a few more tools to work with the NetBox UI:
|
||||
|
||||
- [NodeJS](https://nodejs.org/en/download/) (the LTS release should suffice)
|
||||
- [Yarn](https://yarnpkg.com/getting-started/install) (version 1)
|
||||
|
||||
After Node and Yarn are installed on your system, you'll need to install all the NetBox UI dependencies:
|
||||
|
||||
```console
|
||||
$ cd netbox/project-static
|
||||
$ yarn
|
||||
```
|
||||
|
||||
!!! warning "Check Your Working Directory"
|
||||
You need to be in the `netbox/project-static` directory to run the below `yarn` commands.
|
||||
|
||||
### Bundling
|
||||
|
||||
In order for the TypeScript and Sass (SCSS) source files to be usable by a browser, they must first be transpiled (TypeScript → JavaScript, Sass → CSS), bundled, and minified. After making changes to TypeScript or Sass source files, run `yarn bundle`.
|
||||
|
||||
`yarn bundle` is a wrapper around the following subcommands, any of which can be run individually:
|
||||
|
||||
| Command | Action |
|
||||
| :-------------------- | :---------------------------------------------- |
|
||||
| `yarn bundle` | Bundle TypeScript and Sass (SCSS) source files. |
|
||||
| `yarn bundle:styles` | Bundle Sass (SCSS) source files only. |
|
||||
| `yarn bundle:scripts` | Bundle TypeScript source files only. |
|
||||
|
||||
All output files will be written to `netbox/project-static/dist`, where Django will pick them up when `manage.py collectstatic` is run.
|
||||
|
||||
!!! info "Remember to re-run `manage.py collectstatic`"
|
||||
If you're running the development web server — `manage.py runserver` — you'll need to run `manage.py collectstatic` to see your changes.
|
||||
|
||||
### Linting, Formatting & Type Checking
|
||||
|
||||
Before committing any changes to TypeScript files, and periodically throughout the development process, you should run `yarn validate` to catch formatting, code quality, or type errors.
|
||||
|
||||
!!! tip "IDE Integrations"
|
||||
If you're using an IDE, it is strongly recommended to install [ESLint](https://eslint.org/docs/user-guide/integrations), [TypeScript](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support), and [Prettier](https://prettier.io/docs/en/editors.html) integrations, if available. Most of them will automatically check and/or correct issues in the code as you develop, which can significantly increase your productivity as a contributor.
|
||||
|
||||
`yarn validate` is a wrapper around the following subcommands, any of which can be run individually:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------------------- | :--------------------------------------------------------------- |
|
||||
| `yarn validate` | Run all validation. |
|
||||
| `yarn validate:lint` | Validate TypeScript code via [ESLint](https://eslint.org/) only. |
|
||||
| `yarn validate:types` | Validate TypeScript code compilation only. |
|
||||
| `yarn validate:formatting` | Validate code formatting of JavaScript & Sass/SCSS files. |
|
||||
| `yarn validate:formatting:styles` | Validate code formatting Sass/SCSS only. |
|
||||
| `yarn validate:formatting:scripts` | Validate code formatting TypeScript only. |
|
||||
|
||||
You can also run the following commands to automatically fix formatting issues:
|
||||
|
||||
| Command | Action |
|
||||
| :-------------------- | :---------------------------------------------- |
|
||||
| `yarn format` | Format TypeScript and Sass (SCSS) source files. |
|
||||
| `yarn format:styles` | Format Sass (SCSS) source files only. |
|
||||
| `yarn format:scripts` | Format TypeScript source files only. |
|
||||
|
||||
@@ -11,9 +11,19 @@ table {
|
||||
width: 100%;
|
||||
}
|
||||
th {
|
||||
background-color: #f0f0f0;
|
||||
padding: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
td {
|
||||
padding: 6px;
|
||||
}
|
||||
/* Remove table header coloring. */
|
||||
.md-typeset table:not([class]) th {
|
||||
color: unset !important;
|
||||
background-color: unset !important;
|
||||
}
|
||||
thead tr {
|
||||
/* Colorize table headers. */
|
||||
background-color: var(--md-code-bg-color);
|
||||
color: var(--md-code-fg-color);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -40,28 +40,28 @@ sudo systemctl enable postgresql
|
||||
|
||||
## Database Creation
|
||||
|
||||
At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. This is done with the following commands.
|
||||
At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. Start by invoking the PostgreSQL shell as the system Postgres user.
|
||||
|
||||
```no-highlight
|
||||
sudo -u postgres psql
|
||||
```
|
||||
|
||||
Within the shell, enter the following commands to create the database and user (role), substituting your own value for the password:
|
||||
|
||||
```postgresql
|
||||
CREATE DATABASE netbox;
|
||||
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
|
||||
GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
|
||||
```
|
||||
|
||||
!!! danger
|
||||
**Do not use the password from the example.** Choose a strong, random password to ensure secure database authentication for your NetBox installation.
|
||||
|
||||
```no-highlight
|
||||
$ sudo -u postgres psql
|
||||
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
||||
Type "help" for help.
|
||||
|
||||
postgres=# CREATE DATABASE netbox;
|
||||
CREATE DATABASE
|
||||
postgres=# CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
|
||||
CREATE ROLE
|
||||
postgres=# GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
|
||||
GRANT
|
||||
postgres=# \q
|
||||
```
|
||||
Once complete, enter `\q` to exit the PostgreSQL shell.
|
||||
|
||||
## Verify Service Status
|
||||
|
||||
You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.)
|
||||
You can verify that authentication works by executing the `psql` command and passing the configured username and password. (Replace `localhost` with your database server if using a remote database.)
|
||||
|
||||
```no-highlight
|
||||
$ psql --username netbox --password --host localhost netbox
|
||||
|
||||
@@ -28,6 +28,7 @@ You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/red
|
||||
Use the `redis-cli` utility to ensure the Redis service is functional:
|
||||
|
||||
```no-highlight
|
||||
$ redis-cli ping
|
||||
PONG
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
If successful, you should receive a `PONG` response from the server.
|
||||
|
||||
@@ -36,23 +36,21 @@ This documentation provides two options for installing NetBox: from a downloadab
|
||||
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox` as the NetBox root.
|
||||
|
||||
```no-highlight
|
||||
$ sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||
$ sudo tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||
$ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
||||
$ ls -l /opt | grep netbox
|
||||
lrwxrwxrwx 1 root root 13 Jul 20 13:44 netbox -> netbox-2.9.0/
|
||||
drwxr-xr-x 2 root root 4096 Jul 20 13:44 netbox-2.9.0
|
||||
sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||
sudo tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||
sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
||||
```
|
||||
|
||||
!!! note
|
||||
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v2.9.0 would be installed into `/opt/netbox-2.9.0`, and a symlink from `/opt/netbox/` would point to this location. This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
||||
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v3.0.0 would be installed into `/opt/netbox-3.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated.
|
||||
|
||||
### Option B: Clone the Git Repository
|
||||
|
||||
Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`.
|
||||
|
||||
```no-highlight
|
||||
sudo mkdir -p /opt/netbox/ && cd /opt/netbox/
|
||||
sudo mkdir -p /opt/netbox/
|
||||
cd /opt/netbox/
|
||||
```
|
||||
|
||||
If `git` is not already installed, install it:
|
||||
@@ -72,19 +70,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 .
|
||||
```
|
||||
|
||||
The screen below should be the result:
|
||||
!!! 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
|
||||
@@ -200,7 +201,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
|
||||
|
||||
### NAPALM
|
||||
|
||||
The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
|
||||
Integration with the [NAPALM automation](../additional-features/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
|
||||
@@ -250,23 +251,18 @@ Once the virtual environment has been activated, you should notice the string `(
|
||||
Next, we'll create a superuser account using the `createsuperuser` Django management command (via `manage.py`). Specifying an email address for the user is not required, but be sure to use a very strong password.
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox
|
||||
(venv) $ python3 manage.py createsuperuser
|
||||
Username: admin
|
||||
Email address: admin@example.com
|
||||
Password:
|
||||
Password (again):
|
||||
Superuser created successfully.
|
||||
cd /opt/netbox/netbox
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Schedule the Housekeeping Task
|
||||
|
||||
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/
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
@@ -276,13 +272,19 @@ See the [housekeeping documentation](../administration/housekeeping.md) for furt
|
||||
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ python3 manage.py runserver 0.0.0.0:8000 --insecure
|
||||
python3 manage.py runserver 0.0.0.0:8000 --insecure
|
||||
```
|
||||
|
||||
If successful, you should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
November 17, 2020 - 16:08:13
|
||||
Django version 3.1.3, using settings 'netbox.settings'
|
||||
Starting development server at http://0.0.0.0:8000/
|
||||
August 30, 2021 - 18:02:23
|
||||
Django version 3.2.6, using settings 'netbox.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
|
||||
@@ -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/
|
||||
@@ -31,18 +31,23 @@ sudo systemctl enable netbox netbox-rq
|
||||
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||
|
||||
```no-highlight
|
||||
# systemctl status netbox.service
|
||||
systemctl status netbox.service
|
||||
```
|
||||
|
||||
You should see output similar to the following:
|
||||
|
||||
```no-highlight
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Tue 2020-11-17 16:18:23 UTC; 3min 35s ago
|
||||
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Docs: https://netbox.readthedocs.io/en/stable/
|
||||
Main PID: 22836 (gunicorn)
|
||||
Tasks: 6 (limit: 2345)
|
||||
Memory: 339.3M
|
||||
Main PID: 1140492 (gunicorn)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─22836 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
|
||||
├─22854 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
|
||||
├─22855 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid>
|
||||
├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||
├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||
├─1140514 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va>
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -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/
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
|
Before Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 769 KiB |
|
Before Width: | Height: | Size: 819 KiB |
|
Before Width: | Height: | Size: 916 KiB |
|
Before Width: | Height: | Size: 911 KiB |
|
Before Width: | Height: | Size: 559 KiB |
|
Before Width: | Height: | Size: 569 KiB |
BIN
docs/media/screenshots/cable-trace.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/media/screenshots/home-dark.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
docs/media/screenshots/home-light.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
docs/media/screenshots/prefixes-list.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/media/screenshots/rack.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -25,7 +25,7 @@ A cable may be traced from either of its endpoints by clicking the "trace" butto
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -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](https://napalm-automation.net/) 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
@@ -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
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes/<pk>/available-ips/`. A
|
||||
|
||||
#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
|
||||
|
||||
The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
|
||||
The [NAPALM automation](https://github.com/napalm-automation/napalm) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# NetBox v2.11
|
||||
|
||||
## v2.11.12 (2021-08-23)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list
|
||||
* [#6790](https://github.com/netbox-community/netbox/issues/6790) - Recognize a /32 IPv4 address as a child of a /32 IPv4 prefix
|
||||
* [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view
|
||||
* [#6929](https://github.com/netbox-community/netbox/issues/6929) - Introduce `LOGIN_PERSISTENCE` configuration parameter to persist user sessions
|
||||
* [#7011](https://github.com/netbox-community/netbox/issues/7011) - Add search field to VM interfaces filter form
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
|
||||
* [#6326](https://github.com/netbox-community/netbox/issues/6326) - Enable filtering assigned VLANs by group in interface edit form
|
||||
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
|
||||
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
|
||||
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
|
||||
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list
|
||||
|
||||
---
|
||||
|
||||
## v2.11.11 (2021-08-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -1,29 +1,204 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## v3.0-beta2 (2021-08-13)
|
||||
## v3.0.8 (2021-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6829](https://github.com/netbox-community/netbox/issues/6829) - Extend GraphQL API to support reverse generic relationships
|
||||
* [#6931](https://github.com/netbox-community/netbox/issues/6931) - Include applied filters on object list view
|
||||
* [#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
|
||||
|
||||
* [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users
|
||||
* [#6827](https://github.com/netbox-community/netbox/issues/6827) - Fix circuit termination connection dropdown
|
||||
* [#6832](https://github.com/netbox-community/netbox/issues/6832) - Support config context rendering under GraphQL API
|
||||
* [#6846](https://github.com/netbox-community/netbox/issues/6846) - Form-driven REST API calls should use brief mode
|
||||
* [#6871](https://github.com/netbox-community/netbox/issues/6871) - Support dynamic tag types in GraphQL API
|
||||
* [#6894](https://github.com/netbox-community/netbox/issues/6894) - Fix available IP generation for prefix assigned to a VRF
|
||||
* [#6934](https://github.com/netbox-community/netbox/issues/6934) - Correct prefix utilization and available IP reporting to account for child IP ranges
|
||||
* [#6953](https://github.com/netbox-community/netbox/issues/6953) - Remove change log tab from non-applicable object views
|
||||
* [#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-beta1 (2021-07-23)
|
||||
## 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"
|
||||
Upgrading an existing NetBox deployment to version 3.0 **must** be done from version 2.11.0 or later. If attempting to upgrade a deployment of NetBox v2.10 or earlier, first upgrade to a NetBox v2.11 release, and then upgrade from v2.11 to v3.0. This will avoid any problems with the database migration optimizations implemented in version 3.0.
|
||||
Upgrading an existing NetBox deployment to version 3.0 **must** be done from version 2.11.0 or later. If attempting to upgrade a deployment of NetBox v2.10 or earlier, first upgrade to a NetBox v2.11 release, and then upgrade from v2.11 to v3.0. This will avoid any problems with the database migration optimizations implemented in version 3.0. (This is not necessary for _new_ installations.)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -198,6 +373,16 @@ 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)
|
||||
|
||||
* [#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
|
||||
* [#6982](https://github.com/netbox-community/netbox/issues/6982) - Fix styling of empty dropdown list under dark mode
|
||||
* [#6996](https://github.com/netbox-community/netbox/issues/6996) - Global search bar should be full width on mobile
|
||||
* [#7001](https://github.com/netbox-community/netbox/issues/7001) - Fix page focus on load
|
||||
* [#7034](https://github.com/netbox-community/netbox/issues/7034) - Fix toggling of VLAN group scope selector fields
|
||||
* [#7045](https://github.com/netbox-community/netbox/issues/7045) - Fix navigation menu rendering under Chrome
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#5223](https://github.com/netbox-community/netbox/issues/5223) - Remove the console/power/interface connections REST API endpoints
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Screenshots
|
||||
|
||||
## Light Mode
|
||||
|
||||
### Home Page
|
||||
|
||||

|
||||
|
||||
### Rack Elevation
|
||||
|
||||

|
||||
|
||||
### Prefixes
|
||||
|
||||

|
||||
|
||||
### Cable Trace
|
||||

|
||||
|
||||
## Dark Mode
|
||||
|
||||
### Home Page
|
||||
|
||||

|
||||
|
||||
### Rack Elevation
|
||||
|
||||

|
||||
|
||||
### Prefixes
|
||||
|
||||

|
||||
|
||||
### Cable Trace
|
||||

|
||||
|
||||
10
mkdocs.yml
@@ -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'
|
||||
@@ -103,6 +101,7 @@ nav:
|
||||
- Signals: 'development/signals.md'
|
||||
- Application Registry: 'development/application-registry.md'
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Web UI: 'development/web-ui.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- Release Notes:
|
||||
- Version 3.0: 'release-notes/version-3.0.md'
|
||||
@@ -118,4 +117,3 @@ nav:
|
||||
- Version 2.2: 'release-notes/version-2.2.md'
|
||||
- Version 2.1: 'release-notes/version-2.1.md'
|
||||
- Version 2.0: 'release-notes/version-2.0.md'
|
||||
- Screenshots: 'screenshots/index.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,489 +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'],
|
||||
['region_id', 'site_id'],
|
||||
['asn', 'tag'],
|
||||
]
|
||||
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_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_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_order = ['q', 'provider_id', 'tag']
|
||||
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_order = [
|
||||
'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id',
|
||||
'tenant_id', 'commit_rate',
|
||||
]
|
||||
field_groups = [
|
||||
['q'],
|
||||
['type_id', 'status', 'commit_rate'],
|
||||
['provider_id', 'provider_network_id'],
|
||||
['region_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['tag']
|
||||
]
|
||||
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_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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', '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', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
@@ -105,6 +116,7 @@ class CircuitTable(BaseTable):
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side Z'
|
||||
)
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
@@ -113,7 +125,7 @@ class CircuitTable(BaseTable):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'tags',
|
||||
'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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -192,6 +192,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 = (
|
||||
@@ -210,6 +211,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 +318,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 +339,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,6 +425,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_CS8465C, 'CS8465C'),
|
||||
)),
|
||||
('International/ITA', (
|
||||
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/4)'),
|
||||
(TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'),
|
||||
@@ -442,6 +447,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'),
|
||||
)),
|
||||
@@ -553,6 +559,8 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
# Proprietary
|
||||
TYPE_HDOT_CX = 'hdot-cx'
|
||||
TYPE_SAF_D_GRID = 'saf-d-grid'
|
||||
# Other
|
||||
TYPE_HARDWIRED = 'hardwired'
|
||||
|
||||
CHOICES = (
|
||||
('IEC 60320', (
|
||||
@@ -654,6 +662,9 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_HDOT_CX, 'HDOT Cx'),
|
||||
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_HARDWIRED, 'Hardwired'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
@@ -674,6 +685,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
|
||||
@@ -756,6 +779,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'
|
||||
@@ -880,6 +906,12 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_E3, 'E3 (34 Mbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'ATM',
|
||||
(
|
||||
(TYPE_XDSL, 'xDSL'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Stacking',
|
||||
(
|
||||
@@ -953,6 +985,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 = (
|
||||
(
|
||||
@@ -993,6 +1030,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):
|
||||
|
||||
@@ -831,6 +840,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
location_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__location',
|
||||
queryset=Location.objects.all(),
|
||||
label='Location (ID)',
|
||||
)
|
||||
location = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__location__slug',
|
||||
queryset=Location.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Location (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
@@ -1053,39 +1073,6 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
@@ -1206,6 +1193,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
|
||||
)
|
||||
@@ -1250,7 +1241,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():
|
||||
@@ -1265,73 +1256,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',
|
||||
@@ -1463,3 +1387,52 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
#
|
||||
# Connection filter sets
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
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})
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
5471
netbox/dcim/forms.py
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
@@ -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
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
@@ -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"
|
||||
})
|
||||
289
netbox/dcim/forms/connections.py
Normal file
@@ -0,0 +1,289 @@
|
||||
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:
|
||||
model = Cable
|
||||
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:
|
||||
model = Cable
|
||||
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
@@ -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
|
||||
1148
netbox/dcim/forms/filtersets.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
614
netbox/dcim/forms/object_create.py
Normal file
@@ -0,0 +1,614 @@
|
||||
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 save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign VC members
|
||||
if instance.pk:
|
||||
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||
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
@@ -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(
|
||||
@@ -482,7 +492,7 @@ class CableTraceSVG:
|
||||
)
|
||||
parent_objects.append(parent_object)
|
||||
|
||||
else:
|
||||
elif far_end:
|
||||
|
||||
# Attachment
|
||||
attachment = self._draw_attachment()
|
||||
|
||||