mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-03 23:49:31 +01:00
Compare commits
402 Commits
circuit-sw
...
v4.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de1c5120dd | ||
|
|
87d2e02c85 | ||
|
|
cbbc4f74b8 | ||
|
|
be5bd74d4e | ||
|
|
cf12bb5bf5 | ||
|
|
c060eef1d8 | ||
|
|
96f0debe6e | ||
|
|
b26c7f34cd | ||
|
|
d6428c6aa4 | ||
|
|
e3eca98897 | ||
|
|
cdc735fe41 | ||
|
|
aa4a9da955 | ||
|
|
5c6fc2fb6f | ||
|
|
ad29cb2d66 | ||
|
|
bec5ecf6a9 | ||
|
|
c98f55dbd2 | ||
|
|
dfe20532a1 | ||
|
|
359179fd4a | ||
|
|
c44e8606f7 | ||
|
|
8e620ef325 | ||
|
|
1526e437f1 | ||
|
|
0b507eb207 | ||
|
|
5a36e79215 | ||
|
|
2a0f26623b | ||
|
|
43ae52089f | ||
|
|
1a603981b2 | ||
|
|
245495b2fe | ||
|
|
8d3eb69055 | ||
|
|
7e3b60f194 | ||
|
|
5338c842b8 | ||
|
|
9186b0edaa | ||
|
|
d883be9e56 | ||
|
|
6fc7fa6c64 | ||
|
|
3a33df0e43 | ||
|
|
433f46746e | ||
|
|
8f5f91fcfe | ||
|
|
1a2175127e | ||
|
|
e859807d1d | ||
|
|
a8c997ff29 | ||
|
|
4a28ab98f4 | ||
|
|
3636d55017 | ||
|
|
aa69e96818 | ||
|
|
1745d2ae93 | ||
|
|
e097a848dc | ||
|
|
595be6dcd4 | ||
|
|
a9e50238eb | ||
|
|
a9a300197a | ||
|
|
3dcca73ecc | ||
|
|
cedbeb7b19 | ||
|
|
a45b6b170d | ||
|
|
4b4c542dce | ||
|
|
077d9b1129 | ||
|
|
e81ccb9be6 | ||
|
|
bc83d04c8f | ||
|
|
42ecf3cac0 | ||
|
|
339ad455e4 | ||
|
|
af8e53d8fb | ||
|
|
f24376cfab | ||
|
|
47d4ae29c1 | ||
|
|
8fce672682 | ||
|
|
f776b97415 | ||
|
|
3cc1f30287 | ||
|
|
6d166aa10d | ||
|
|
040a2ae9a9 | ||
|
|
39f11f28fb | ||
|
|
62b9025a9e | ||
|
|
21091f22e6 | ||
|
|
3efa23cf8f | ||
|
|
0f62137957 | ||
|
|
7858ccb712 | ||
|
|
6b7b38ee0a | ||
|
|
c8f17e06a2 | ||
|
|
edace6aff4 | ||
|
|
586bc132b6 | ||
|
|
52a2b934a0 | ||
|
|
3d1f18d6dd | ||
|
|
3e2a26984f | ||
|
|
f5f0c19860 | ||
|
|
8da9b11ab8 | ||
|
|
ca67fa9999 | ||
|
|
eff768192e | ||
|
|
1e297d55ee | ||
|
|
fdb987ef91 | ||
|
|
b5a23db43c | ||
|
|
366b69aff7 | ||
|
|
c3e8c5e69c | ||
|
|
b55f36469d | ||
|
|
1c46215cd5 | ||
|
|
7fded2fd87 | ||
|
|
0ddc5805c4 | ||
|
|
c1bbc026e2 | ||
|
|
8cbfe94fba | ||
|
|
434334d927 | ||
|
|
fff99fd3ff | ||
|
|
6bd083b7ed | ||
|
|
f38faf2e01 | ||
|
|
f4892caa51 | ||
|
|
e60807adc5 | ||
|
|
e14934e5a5 | ||
|
|
ae03723e43 | ||
|
|
c0f79df91f | ||
|
|
edbfd0bae6 | ||
|
|
c3e111c769 | ||
|
|
c11f4b3716 | ||
|
|
a54ad24b47 | ||
|
|
3624b88c3f | ||
|
|
f54ed8bb7f | ||
|
|
5d0609e729 | ||
|
|
865b88e724 | ||
|
|
e73db97d46 | ||
|
|
6f2ba5c75c | ||
|
|
fa8a9ef9de | ||
|
|
6beb079b97 | ||
|
|
bad688b8aa | ||
|
|
c8aad24a1b | ||
|
|
42bd876604 | ||
|
|
f903442cb9 | ||
|
|
5a64cb712d | ||
|
|
4d90d559be | ||
|
|
19de058f94 | ||
|
|
d3e4c02807 | ||
|
|
dc00e19c3c | ||
|
|
6ed6da49d9 | ||
|
|
7154d4ae2e | ||
|
|
bc26529be8 | ||
|
|
da64c564ae | ||
|
|
6199b3e039 | ||
|
|
ebada4bf72 | ||
|
|
2a391253a5 | ||
|
|
914653d63e | ||
|
|
3813aad8b1 | ||
|
|
ea5371040e | ||
|
|
6c824cc48f | ||
|
|
c78b8401dc | ||
|
|
f510e40428 | ||
|
|
860db9590b | ||
|
|
7c63d001b1 | ||
|
|
93119f52c3 | ||
|
|
ee2aa35cba | ||
|
|
edf35e35be | ||
|
|
7896a48075 | ||
|
|
eb87c3f304 | ||
|
|
062a871521 | ||
|
|
3acbb0a08c | ||
|
|
f67cc47def | ||
|
|
f7219e0672 | ||
|
|
e5a975176d | ||
|
|
07d8157ccd | ||
|
|
83ee4fb593 | ||
|
|
db8271c904 | ||
|
|
712c743bcb | ||
|
|
2eb42d4907 | ||
|
|
5a24f99c9d | ||
|
|
9318c91405 | ||
|
|
5c6aaf2388 | ||
|
|
265f375595 | ||
|
|
a28269b73a | ||
|
|
d95fa8dbb2 | ||
|
|
2699149016 | ||
|
|
f371004809 | ||
|
|
44e731a40a | ||
|
|
a364ee832d | ||
|
|
875e3e7979 | ||
|
|
ad29402b87 | ||
|
|
598f8d034d | ||
|
|
ec13a79907 | ||
|
|
21f4036782 | ||
|
|
ce3738572c | ||
|
|
cbb979934e | ||
|
|
642d83a4c6 | ||
|
|
3140060f21 | ||
|
|
607a385a12 | ||
|
|
834da4e6cd | ||
|
|
a06c12c6b8 | ||
|
|
60fce84c96 | ||
|
|
8719fd4a54 | ||
|
|
59afa0b41d | ||
|
|
14b246cb8a | ||
|
|
f0507d00bf | ||
|
|
77b389f105 | ||
|
|
f56015e03d | ||
|
|
dc09ec3025 | ||
|
|
4e0265a001 | ||
|
|
113c8b7ae6 | ||
|
|
17d8f78ae3 | ||
|
|
97d0a16fd4 | ||
|
|
174b2d5f39 | ||
|
|
970f2bd4ed | ||
|
|
a4ee323cb6 | ||
|
|
17e5184a11 | ||
|
|
e1548bb290 | ||
|
|
cc935dbfab | ||
|
|
27ffc3df6a | ||
|
|
7bf84eb400 | ||
|
|
e910d461ea | ||
|
|
269112a565 | ||
|
|
3483d979d4 | ||
|
|
ca43adf692 | ||
|
|
c6672538ac | ||
|
|
9ae53fc232 | ||
|
|
7eefb07554 | ||
|
|
6efb258b9f | ||
|
|
20c260b126 | ||
|
|
da1e0f4b53 | ||
|
|
7f39f75d3d | ||
|
|
7bca9f5d6d | ||
|
|
ebf8f7fa1b | ||
|
|
922b08c0ff | ||
|
|
84864fa5e1 | ||
|
|
767dfccd8f | ||
|
|
502b33b144 | ||
|
|
10e69c8b30 | ||
|
|
513b11450d | ||
|
|
b5edfa5d53 | ||
|
|
dc4bab7477 | ||
|
|
60aa952eb1 | ||
|
|
afba5b2791 | ||
|
|
8b3f7ce507 | ||
|
|
adad3745ae | ||
|
|
8055fae253 | ||
|
|
1505285aff | ||
|
|
7cc7c7ab81 | ||
|
|
ae21a6a684 | ||
|
|
aac3a51431 | ||
|
|
3e0ad2176f | ||
|
|
4e8edfb3d6 | ||
|
|
651557a82b | ||
|
|
c3d66dc42e | ||
|
|
a50e570f22 | ||
|
|
a44a79ec79 | ||
|
|
b919868521 | ||
|
|
d9aab6bbe2 | ||
|
|
82171fce7a | ||
|
|
1dcfc05c32 | ||
|
|
5143003c68 | ||
|
|
020eb64eab | ||
|
|
ec7afccd55 | ||
|
|
76fd63823c | ||
|
|
6c373decd6 | ||
|
|
222b26e060 | ||
|
|
066b787777 | ||
|
|
90b2732068 | ||
|
|
bfba0ccaae | ||
|
|
d5718357f1 | ||
|
|
d61737396b | ||
|
|
c6248f1142 | ||
|
|
05f254a768 | ||
|
|
0cb10f806a | ||
|
|
8ac7f6f8de | ||
|
|
45fc354d45 | ||
|
|
cd8087ab43 | ||
|
|
da5ae21150 | ||
|
|
38b2839a1e | ||
|
|
5585b410f8 | ||
|
|
47ac506d5c | ||
|
|
db3a4bc731 | ||
|
|
ebeceaaa21 | ||
|
|
3e1ccc80e9 | ||
|
|
d192c1e352 | ||
|
|
c7d94bd529 | ||
|
|
a718cb1173 | ||
|
|
867a01fae5 | ||
|
|
fbb948d30e | ||
|
|
975e0ff398 | ||
|
|
d7877b7627 | ||
|
|
b685df7c9c | ||
|
|
9dcf9475cc | ||
|
|
cee2a5e0ed | ||
|
|
e1bf27e4db | ||
|
|
9b89af75e4 | ||
|
|
9e13d89baa | ||
|
|
01cbdbb968 | ||
|
|
a4365be0a3 | ||
|
|
4961b0d334 | ||
|
|
ab06edd9f5 | ||
|
|
e787a71c1d | ||
|
|
cd8878df30 | ||
|
|
b5a9cb1762 | ||
|
|
1d2f6a82cb | ||
|
|
6e7bbfc3e2 | ||
|
|
9723a2f0ad | ||
|
|
327d08f4c2 | ||
|
|
3e43226901 | ||
|
|
7b0e8c1a0d | ||
|
|
917280d1d3 | ||
|
|
4be476eb49 | ||
|
|
8005b56ab4 | ||
|
|
3f1654c9ba | ||
|
|
95f8fe788d | ||
|
|
588c069ff1 | ||
|
|
5b3ff3c0e9 | ||
|
|
a024012abd | ||
|
|
730d73042d | ||
|
|
6c2a6d0e90 | ||
|
|
6fc04bd1fe | ||
|
|
e55a4ae603 | ||
|
|
60cc009d6b | ||
|
|
e9777d3193 | ||
|
|
1d2aef71b2 | ||
|
|
4edaa48aa7 | ||
|
|
dfb08ff521 | ||
|
|
9d6522c11e | ||
|
|
281cb4f586 | ||
|
|
838794a5cf | ||
|
|
e6a6ff7aec | ||
|
|
1de41b4964 | ||
|
|
d5cec3723e | ||
|
|
87ff83ef1f | ||
|
|
59899d0d9a | ||
|
|
bcffc383bf | ||
|
|
3cdc6251be | ||
|
|
0e1705b870 | ||
|
|
8522c03b71 | ||
|
|
20af97ce24 | ||
|
|
c05106f9b2 | ||
|
|
264b40a269 | ||
|
|
17429c4257 | ||
|
|
40b114c0bb | ||
|
|
1cffbb21bb | ||
|
|
ed3dd019a7 | ||
|
|
17cffd7860 | ||
|
|
21bb734dcb | ||
|
|
c392988212 | ||
|
|
37bea1e98e | ||
|
|
cbf9b62f12 | ||
|
|
c429cc3638 | ||
|
|
da68503a19 | ||
|
|
e9b15436c4 | ||
|
|
4d5f8e9460 | ||
|
|
77613b37b2 | ||
|
|
3fd4664a76 | ||
|
|
032ed4f11c | ||
|
|
7ca4342c15 | ||
|
|
70bc1c226a | ||
|
|
eef9db5e5a | ||
|
|
6a21459ccc | ||
|
|
635de4af2e | ||
|
|
df96f7dd0f | ||
|
|
90712fa865 | ||
|
|
90874adf14 | ||
|
|
2a629d6f74 | ||
|
|
83de784196 | ||
|
|
1acd567706 | ||
|
|
7d993cc141 | ||
|
|
d4783b7fbd | ||
|
|
3890043b06 | ||
|
|
fbe76ac98a | ||
|
|
0b61d69e05 | ||
|
|
1245a9f99d | ||
|
|
78223cea03 | ||
|
|
8452222761 | ||
|
|
8a59fc733c | ||
|
|
df688ce064 | ||
|
|
fd3a9a0c37 | ||
|
|
1a1ab2a19d | ||
|
|
068d493cc6 | ||
|
|
80f03daad6 | ||
|
|
d04c41d0f6 | ||
|
|
1fc849eb40 | ||
|
|
bbf1f6181d | ||
|
|
729b0365e0 | ||
|
|
43cb476223 | ||
|
|
d6f756d315 | ||
|
|
afc62b6ffd | ||
|
|
3d4841f17f | ||
|
|
2aefb3af73 | ||
|
|
4eff4d6a4a | ||
|
|
9381564cab | ||
|
|
3d143d635b | ||
|
|
77307b3c91 | ||
|
|
bf83299a93 | ||
|
|
aa4571b61f | ||
|
|
56d9146323 | ||
|
|
e192f64dd2 | ||
|
|
d433a28524 | ||
|
|
dbfdf318ad | ||
|
|
639bc4462b | ||
|
|
1c59d411f7 | ||
|
|
ac7a4ec4a3 | ||
|
|
0cf58e62b2 | ||
|
|
fb8d41b527 | ||
|
|
ae5d7911f9 | ||
|
|
3bd0186870 | ||
|
|
09ce8a808d | ||
|
|
8eaff9dce7 | ||
|
|
cb3308a166 | ||
|
|
5fbae8407e | ||
|
|
2fdd46f64c | ||
|
|
c5124cb2e4 | ||
|
|
d01d7b4156 | ||
|
|
4db6123fb2 | ||
|
|
43648d629b | ||
|
|
0b97df0984 | ||
|
|
5334c8143c | ||
|
|
bbb330becf | ||
|
|
e4c74ce6a3 | ||
|
|
a4868f894d | ||
|
|
531ea34207 | ||
|
|
6747c82a1a | ||
|
|
e251ea10b5 | ||
|
|
a1aaf465ac | ||
|
|
2a1d315d85 |
@@ -2,7 +2,7 @@
|
||||
name: ✨ Feature Request
|
||||
type: Feature
|
||||
description: Propose a new NetBox feature or enhancement
|
||||
labels: ["type: feature", "status: needs triage"]
|
||||
labels: ["netbox", "type: feature", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.4
|
||||
placeholder: v4.5.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -2,7 +2,7 @@
|
||||
name: 🐛 Bug Report
|
||||
type: Bug
|
||||
description: Report a reproducible bug in the current release of NetBox
|
||||
labels: ["type: bug", "status: needs triage"]
|
||||
labels: ["netbox", "type: bug", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.4.4
|
||||
placeholder: v4.5.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: 🏁 Performance
|
||||
type: Performance
|
||||
description: An opportunity to improve application performance
|
||||
labels: ["netbox", "type: performance", "status: needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Area(s) of Concern
|
||||
description: Which application interface(s) are affected?
|
||||
options:
|
||||
- label: User Interface
|
||||
- label: REST API
|
||||
- label: GraphQL API
|
||||
- label: Python ORM
|
||||
- label: Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: >
|
||||
Describe in detail the operations being performed and the indications of a performance issue.
|
||||
Include any relevant testing parameters, benchmarks, and expected results.
|
||||
validations:
|
||||
required: true
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 📖 Documentation Change
|
||||
type: Documentation
|
||||
description: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation", "status: needs triage"]
|
||||
labels: ["netbox", "type: documentation", "status: needs triage"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 🌍 Translation
|
||||
type: Translation
|
||||
description: Request support for a new language in the user interface
|
||||
labels: ["type: translation"]
|
||||
labels: ["netbox", "type: translation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
25
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
25
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: 🗑️ Deprecation
|
||||
type: Deprecation
|
||||
description: The removal of an existing feature or resource
|
||||
labels: ["type: deprecation"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Changes
|
||||
description: >
|
||||
Describe in detail the proposed changes. What is being removed?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Justification
|
||||
description: Please provide justification for the proposed change(s).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Impact
|
||||
description: List all areas of the application that will be affected by this change.
|
||||
validations:
|
||||
required: true
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 🏡 Housekeeping
|
||||
type: Housekeeping
|
||||
description: A change pertaining to the codebase itself (developers only)
|
||||
labels: ["type: housekeeping"]
|
||||
labels: ["netbox", "type: housekeeping"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
31
.github/ISSUE_TEMPLATE/07-deprecation.yaml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/07-deprecation.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: ⚠️ Deprecation
|
||||
type: Deprecation
|
||||
description: Designation of a feature or behavior that will be removed in a future release
|
||||
labels: ["netbox", "type: deprecation"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Deprecated Functionality
|
||||
description: >
|
||||
Describe the feature(s) and/or behavior that is being flagged for deprecation.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Scheduled removal
|
||||
description: In what future release will the deprecated functionality be removed?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Justification
|
||||
description: Please provide justification for the deprecation.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Impact
|
||||
description: List all areas of the application that will be affected by this change.
|
||||
validations:
|
||||
required: true
|
||||
20
.github/ISSUE_TEMPLATE/08-feature_removal.yaml
vendored
Normal file
20
.github/ISSUE_TEMPLATE/08-feature_removal.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 🗑️ Feature Removal
|
||||
type: Removal
|
||||
description: The removal of a deprecated feature or resource
|
||||
labels: ["netbox", "type: removal"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Deprecation Issue
|
||||
description: Specify the issue in which this deprecation was announced.
|
||||
placeholder: "#1234"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary of Changes
|
||||
description: >
|
||||
List all changes necessary to remove the deprecated feature or resource.
|
||||
validations:
|
||||
required: true
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12', '3.13']
|
||||
python-version: ['3.12', '3.13', '3.14']
|
||||
node-version: ['20.x']
|
||||
services:
|
||||
redis:
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt install -y gettext
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,7 +9,8 @@ yarn-error.log*
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/local/*
|
||||
/netbox/media
|
||||
/netbox/media/*
|
||||
!/netbox/media/.gitkeep
|
||||
/netbox/reports/*
|
||||
!/netbox/reports/__init__.py
|
||||
/netbox/scripts/*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
rev: v0.14.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Ruff linter"
|
||||
@@ -21,14 +21,6 @@ repos:
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: openapi-check
|
||||
name: "Validate OpenAPI schema"
|
||||
description: "Check for any unexpected changes to the OpenAPI schema"
|
||||
files: api/.*\.py$
|
||||
entry: scripts/verify-openapi.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [python]
|
||||
- id: mkdocs-build
|
||||
name: "Build documentation"
|
||||
description: "Build the documentation with mkdocs"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<a href="https://github.com/netbox-community/netbox/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-16-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
|
||||
<p>
|
||||
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
||||
|
||||
@@ -85,7 +85,7 @@ drf-spectacular-sidecar
|
||||
feedparser
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://docs.gunicorn.org/en/latest/news.html
|
||||
# https://gunicorn.org/news/
|
||||
gunicorn
|
||||
|
||||
# Platform-agnostic template rendering engine
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
"usb-3-micro-b",
|
||||
"molex-micro-fit-1x2",
|
||||
"molex-micro-fit-2x2",
|
||||
"molex-micro-fit-2x3",
|
||||
"molex-micro-fit-2x4",
|
||||
"dc-terminal",
|
||||
"saf-d-grid",
|
||||
@@ -293,6 +294,7 @@
|
||||
"usb-c",
|
||||
"molex-micro-fit-1x2",
|
||||
"molex-micro-fit-2x2",
|
||||
"molex-micro-fit-2x3",
|
||||
"molex-micro-fit-2x4",
|
||||
"dc-terminal",
|
||||
"eaton-c39",
|
||||
|
||||
22349
contrib/openapi.json
22349
contrib/openapi.json
File diff suppressed because one or more lines are too long
@@ -3,29 +3,41 @@
|
||||
NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||
|
||||
```
|
||||
./manage.py nbshell
|
||||
cd /opt/netbox
|
||||
source /opt/netbox/venv/bin/activate
|
||||
python3 netbox/manage.py nbshell
|
||||
```
|
||||
|
||||
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||
This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models preloaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||
|
||||
```
|
||||
$ ./manage.py nbshell
|
||||
(venv) $ python3 netbox/manage.py nbshell
|
||||
### NetBox interactive shell (localhost)
|
||||
### Python 3.7.10 | Django 3.2.5 | NetBox 3.0
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||
### Python v3.12.3 | Django v5.2.10 | NetBox Community v4.5.1
|
||||
### lsapps() & lsmodels() will show available models. Use help(<model>) for more info.
|
||||
```
|
||||
|
||||
The function `lsmodels()` will print a list of all available NetBox models:
|
||||
|
||||
```
|
||||
>>> lsmodels()
|
||||
DCIM:
|
||||
ConsolePort
|
||||
ConsolePortTemplate
|
||||
ConsoleServerPort
|
||||
ConsoleServerPortTemplate
|
||||
Device
|
||||
...
|
||||
DCIM:
|
||||
dcim.Cable
|
||||
dcim.CableTermination
|
||||
dcim.ConsolePort
|
||||
dcim.ConsolePortTemplate
|
||||
dcim.ConsoleServerPort
|
||||
dcim.ConsoleServerPortTemplate
|
||||
dcim.Device
|
||||
...
|
||||
```
|
||||
|
||||
To exit the NetBox shell, type `exit()` or press `Ctrl+D`.
|
||||
|
||||
```
|
||||
>>> exit()
|
||||
(venv) $
|
||||
```
|
||||
|
||||
!!! warning
|
||||
@@ -114,7 +126,7 @@ Reverse relationships can be traversed as well. For example, the following will
|
||||
>>> Device.objects.filter(interfaces__name="em0")
|
||||
```
|
||||
|
||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the latter of which is case-insensitive).
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(name__icontains="testdevice")
|
||||
|
||||
@@ -88,7 +88,7 @@ While permissions are typically assigned to specific groups and/or users, it is
|
||||
|
||||
### Viewing Objects
|
||||
|
||||
Object-based permissions work by filtering the database query generated by a user's request to restrict the set of objects returned. When a request is received, NetBox first determines whether the user is authenticated and has been granted to perform the requested action. For example, if the requested URL is `/dcim/devices/`, NetBox will check for the `dcim.view_device` permission. If the user has not been assigned this permission (either directly or via a group assignment), NetBox will return a 403 (forbidden) HTTP response.
|
||||
Object-based permissions work by filtering the database query generated by a user's request to restrict the set of objects returned. When a request is received, NetBox first determines whether the user is authenticated and has been granted permission to perform the requested action. For example, if the requested URL is `/dcim/devices/`, NetBox will check for the `dcim.view_device` permission. If the user has not been assigned this permission (either directly or via a group assignment), NetBox will return a 403 (forbidden) HTTP response.
|
||||
|
||||
If the permission _has_ been granted, NetBox will compile any specified constraints for the model and action. For example, suppose two permissions have been assigned to the user granting view access to the device model, with the following constraints:
|
||||
|
||||
@@ -102,9 +102,9 @@ If the permission _has_ been granted, NetBox will compile any specified constrai
|
||||
This grants the user access to view any device that is assigned to a site named NYC1 or NYC2, **or** which has a status of "offline" and has no tenant assigned. These constraints are equivalent to the following ORM query:
|
||||
|
||||
```no-highlight
|
||||
Site.objects.filter(
|
||||
Device.objects.filter(
|
||||
Q(site__name__in=['NYC1', 'NYC2']),
|
||||
Q(status='active', tenant__isnull=True)
|
||||
Q(status='offline', tenant__isnull=True)
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
"dcim.Site": [
|
||||
{
|
||||
"name": {
|
||||
"min_length": 5,
|
||||
@@ -17,12 +17,15 @@ CUSTOM_VALIDATORS = {
|
||||
},
|
||||
"my_plugin.validators.Validator1"
|
||||
],
|
||||
"dcim.device": [
|
||||
"dcim.Device": [
|
||||
"my_plugin.validators.Validator1"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Case-Insensitive Model Names"
|
||||
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
||||
|
||||
---
|
||||
|
||||
## FIELD_CHOICES
|
||||
@@ -53,6 +56,9 @@ FIELD_CHOICES = {
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Case-Insensitive Field Identifiers"
|
||||
Field identifiers are case-insensitive. Both `dcim.Site.status` and `dcim.site.status` are valid and equivalent.
|
||||
|
||||
The following model fields support configurable choices:
|
||||
|
||||
* `circuits.Circuit.status`
|
||||
@@ -98,7 +104,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid
|
||||
|
||||
```python
|
||||
PROTECTION_RULES = {
|
||||
"dcim.site": [
|
||||
"dcim.Site": [
|
||||
{
|
||||
"status": {
|
||||
"eq": "decommissioning"
|
||||
@@ -108,3 +114,6 @@ PROTECTION_RULES = {
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Case-Insensitive Model Names"
|
||||
Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent.
|
||||
|
||||
@@ -15,7 +15,7 @@ Some configuration parameters may alternatively be defined either in `configurat
|
||||
|
||||
## Dynamic Configuration Parameters
|
||||
|
||||
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
||||
Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > System > Configuration History). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below:
|
||||
|
||||
* [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes)
|
||||
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
||||
@@ -35,6 +35,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
||||
* [`POWERFEED_DEFAULT_MAX_UTILIZATION`](./default-values.md#powerfeed_default_max_utilization)
|
||||
* [`POWERFEED_DEFAULT_VOLTAGE`](./default-values.md#powerfeed_default_voltage)
|
||||
* [`PREFER_IPV4`](./miscellaneous.md#prefer_ipv4)
|
||||
* [`PROTECTION_RULES`](./data-validation.md#protection_rules)
|
||||
* [`RACK_ELEVATION_DEFAULT_UNIT_HEIGHT`](./default-values.md#rack_elevation_default_unit_height)
|
||||
* [`RACK_ELEVATION_DEFAULT_UNIT_WIDTH`](./default-values.md#rack_elevation_default_unit_width)
|
||||
|
||||
|
||||
@@ -53,6 +53,16 @@ Sets content for the top banner in the user interface.
|
||||
|
||||
---
|
||||
|
||||
## COPILOT_ENABLED
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: `True`
|
||||
|
||||
Enables or disables the [NetBox Copilot](https://netboxlabs.com/docs/copilot/) agent globally. When enabled, users can opt to toggle the agent individually.
|
||||
|
||||
---
|
||||
|
||||
## CENSUS_REPORTING_ENABLED
|
||||
|
||||
Default: `True`
|
||||
|
||||
@@ -81,7 +81,7 @@ If `True`, the cookie employed for cross-site request forgery (CSRF) protection
|
||||
|
||||
Default: `[]`
|
||||
|
||||
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
|
||||
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://`).
|
||||
|
||||
```python
|
||||
CSRF_TRUSTED_ORIGINS = (
|
||||
|
||||
@@ -232,6 +232,9 @@ STORAGES = {
|
||||
},
|
||||
"scripts": {
|
||||
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||
"OPTIONS": {
|
||||
"allow_overwrite": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -247,6 +250,7 @@ STORAGES = {
|
||||
"OPTIONS": {
|
||||
'access_key': 'access key',
|
||||
'secret_key': 'secret key',
|
||||
"allow_overwrite": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ An example fieldset definition is provided below:
|
||||
|
||||
```python
|
||||
class MyScript(Script):
|
||||
class Meta:
|
||||
class Meta(Script.Meta):
|
||||
fieldsets = (
|
||||
('First group', ('field1', 'field2', 'field3')),
|
||||
('Second group', ('field4', 'field5')),
|
||||
@@ -393,6 +393,61 @@ A complete date & time. Returns a `datetime.datetime` object.
|
||||
|
||||
Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button. It is possible to schedule a script to be executed at specified time in the future. A scheduled script can be canceled by deleting the associated job result object.
|
||||
|
||||
#### Prefilling variables via URL parameters
|
||||
|
||||
Script form fields can be prefilled by appending query parameters to the script URL. Each parameter name must match the variable name defined on the script class. Prefilled values are treated as initial values and can be edited before execution. Multiple values can be supplied by repeating the same parameter. Query values must be percent‑encoded where required (for example, spaces as `%20`).
|
||||
|
||||
Examples:
|
||||
|
||||
For string and integer variables, when a script defines:
|
||||
|
||||
```python
|
||||
from extras.scripts import Script, StringVar, IntegerVar
|
||||
|
||||
class MyScript(Script):
|
||||
name = StringVar()
|
||||
count = IntegerVar()
|
||||
```
|
||||
|
||||
the following URL prefills the `name` and `count` fields:
|
||||
|
||||
```
|
||||
https://<netbox>/extras/scripts/<script_id>/?name=Branch42&count=3
|
||||
```
|
||||
|
||||
For object variables (`ObjectVar`), supply the object’s primary key (PK):
|
||||
|
||||
```
|
||||
https://<netbox>/extras/scripts/<script_id>/?device=1
|
||||
```
|
||||
|
||||
If an object ID cannot be resolved or the object is not visible to the requesting user, the field remains unpopulated.
|
||||
|
||||
Supported variable types:
|
||||
|
||||
| Variable class | Expected input | Example query string |
|
||||
|--------------------------|---------------------------------|---------------------------------------------|
|
||||
| `StringVar` | string (percent‑encoded) | `?name=Branch42` |
|
||||
| `TextVar` | string (percent‑encoded) | `?notes=Initial%20value` |
|
||||
| `IntegerVar` | integer | `?count=3` |
|
||||
| `DecimalVar` | decimal number | `?ratio=0.75` |
|
||||
| `BooleanVar` | value → `True`; empty → `False` | `?enabled=true` (True), `?enabled=` (False) |
|
||||
| `ChoiceVar` | choice value (not label) | `?role=edge` |
|
||||
| `MultiChoiceVar` | choice values (repeat) | `?roles=edge&roles=core` |
|
||||
| `ObjectVar(Device)` | PK (integer) | `?device=1` |
|
||||
| `MultiObjectVar(Device)` | PKs (repeat) | `?devices=1&devices=2` |
|
||||
| `IPAddressVar` | IP address | `?ip=198.51.100.10` |
|
||||
| `IPAddressWithMaskVar` | IP address with mask | `?addr=192.0.2.1/24` |
|
||||
| `IPNetworkVar` | IP network prefix | `?network=2001:db8::/64` |
|
||||
| `DateVar` | date `YYYY-MM-DD` | `?date=2025-01-05` |
|
||||
| `DateTimeVar` | ISO datetime | `?when=2025-01-05T14:30:00` |
|
||||
| `FileVar` | — (not supported) | — |
|
||||
|
||||
!!! note
|
||||
- The parameter names above are examples; use the actual variable attribute names defined by the script.
|
||||
- For `BooleanVar`, only an empty value (`?enabled=`) unchecks the box; any other value including `false` or `0` checks it.
|
||||
- File uploads (`FileVar`) cannot be prefilled via URL parameters.
|
||||
|
||||
### Via the API
|
||||
|
||||
To run a script via the REST API, issue a POST request to the script's endpoint specifying the form data and commitment. For example, to run a script named `example.MyReport`, we would make a request such as the following:
|
||||
@@ -444,7 +499,7 @@ from extras.scripts import *
|
||||
|
||||
class NewBranchScript(Script):
|
||||
|
||||
class Meta:
|
||||
class Meta(Script.Meta):
|
||||
name = "New Branch"
|
||||
description = "Provision a new branch site"
|
||||
field_order = ['site_name', 'switch_count', 'switch_model']
|
||||
|
||||
@@ -20,6 +20,10 @@ A dictionary mapping data backend types to their respective classes. These are u
|
||||
|
||||
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
|
||||
|
||||
### `filtersets`
|
||||
|
||||
A dictionary mapping each model (identified by its app and label) to its filterset class, if one has been registered for it. Filtersets are registered using the `@register_filterset` decorator.
|
||||
|
||||
### `model_features`
|
||||
|
||||
A dictionary of model features (e.g. custom fields, tags, etc.) mapped to the functions used to qualify a model as supporting each feature. Model features are registered using the `register_model_feature()` function in `netbox.utils`.
|
||||
|
||||
@@ -12,7 +12,7 @@ Depending on its classification, each NetBox model may support various features
|
||||
|
||||
| Feature | Feature Mixin | Registry Key | Description |
|
||||
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
||||
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||
| [Bookmarks](../features/user-preferences.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
||||
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
||||
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
||||
|
||||
@@ -144,7 +144,7 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
* Update the version number and published date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Copy the version number from `release.yaml` to `pyproject.toml` in the project root.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Update the example version numbers in the feature request, bug report, and performance templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -6,10 +6,14 @@ For end‑user guidance on resetting saved table layouts, see [Features > User P
|
||||
|
||||
## Available Preferences
|
||||
|
||||
| Name | Description |
|
||||
|--------------------------|---------------------------------------------------------------|
|
||||
| data_format | Preferred format when rendering raw data (JSON or YAML) |
|
||||
| pagination.per_page | The number of items to display per page of a paginated table |
|
||||
| pagination.placement | Where to display the paginator controls relative to the table |
|
||||
| tables.${table}.columns | The ordered list of columns to display when viewing the table |
|
||||
| tables.${table}.ordering | A list of column names by which the table should be ordered |
|
||||
| Name | Description |
|
||||
|----------------------------|---------------------------------------------------------------|
|
||||
| `csv_delimiter` | The delimiting character used when exporting CSV data |
|
||||
| `data_format` | Preferred format when rendering raw data (JSON or YAML) |
|
||||
| `locale.language` | The language selected for UI translation |
|
||||
| `pagination.per_page` | The number of items to display per page of a paginated table |
|
||||
| `pagination.placement` | Where to display the paginator controls relative to the table |
|
||||
| `tables.${table}.columns` | The ordered list of columns to display when viewing the table |
|
||||
| `tables.${table}.ordering` | A list of column names by which the table should be ordered |
|
||||
| `ui.copilot_enabled` | Toggles the NetBox Copilot AI agent |
|
||||
| `ui.tables.striping` | Toggles visual striping of tables in the UI |
|
||||
|
||||
@@ -10,9 +10,11 @@ Change records are exposed in the API via the read-only endpoint `/api/extras/ob
|
||||
|
||||
## User Messages
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.4."
|
||||
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message (up to 200 characters) that will appear in the change record. This can be helpful to capture additional context, such as the reason for a change or a reference to an external ticket.
|
||||
|
||||
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change.
|
||||
When editing an object via the web UI, the "Changelog message" field appears at the bottom of the form. This field is optional. The changelog message field is available in object create forms, object edit forms, delete confirmation dialogs, and bulk operations.
|
||||
|
||||
For information on including changelog messages when making changes via the REST API, see [Changelog Messages](../integrations/rest-api.md#changelog-messages).
|
||||
|
||||
## Correlating Changes by Request
|
||||
|
||||
|
||||
@@ -51,14 +51,14 @@ You can verify that authentication works by executing the `psql` command and pas
|
||||
|
||||
```no-highlight
|
||||
$ psql --username netbox --password --host localhost netbox
|
||||
Password for user netbox:
|
||||
psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1))
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
||||
Password:
|
||||
psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1))
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||
Type "help" for help.
|
||||
|
||||
netbox=> \conninfo
|
||||
You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432".
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
|
||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||
netbox=> \q
|
||||
```
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ 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 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.
|
||||
It is recommended to install NetBox in a directory named for its version number. For example, NetBox v4.0.0 would be installed into `/opt/netbox-4.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
|
||||
|
||||
@@ -63,12 +63,12 @@ This command should generate output similar to the following:
|
||||
|
||||
```
|
||||
Cloning into '.'...
|
||||
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.
|
||||
remote: Enumerating objects: 148317, done.
|
||||
remote: Counting objects: 100% (183/183), done.
|
||||
remote: Compressing objects: 100% (115/115), done.
|
||||
remote: Total 148317 (delta 127), reused 68 (delta 68), pack-reused 148134 (from 3)
|
||||
Receiving objects: 100% (148317/148317), 165.12 MiB | 28.71 MiB/s, done.
|
||||
Resolving deltas: 100% (116428/116428), done.
|
||||
```
|
||||
|
||||
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
|
||||
@@ -102,7 +102,8 @@ sudo cp configuration_example.py configuration.py
|
||||
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
|
||||
|
||||
* `ALLOWED_HOSTS`
|
||||
* `DATABASES` (or `DATABASE`)
|
||||
* `API_TOKEN_PEPPERS`
|
||||
* `DATABASES`
|
||||
* `REDIS`
|
||||
* `SECRET_KEY`
|
||||
|
||||
@@ -158,7 +159,7 @@ DATABASES = {
|
||||
|
||||
### REDIS
|
||||
|
||||
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
||||
Redis is an in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters.
|
||||
|
||||
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
|
||||
|
||||
@@ -252,7 +253,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa
|
||||
sudo /opt/netbox/upgrade.sh
|
||||
```
|
||||
|
||||
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||
Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.)
|
||||
|
||||
```no-highlight
|
||||
sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh
|
||||
@@ -295,13 +296,12 @@ 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).
|
||||
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/
|
||||
January 26, 2026 - 17:00:00
|
||||
Django version 5.2.10, using settings 'netbox.settings'
|
||||
Starting development server at http://0.0.0.0:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
|
||||
@@ -43,16 +43,22 @@ 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 Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled)
|
||||
Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago
|
||||
Docs: https://docs.netbox.dev/
|
||||
Main PID: 1140492 (gunicorn)
|
||||
Tasks: 19 (limit: 4683)
|
||||
Memory: 666.2M
|
||||
Main PID: 7283 (gunicorn)
|
||||
Tasks: 6 (limit: 4545)
|
||||
Memory: 556.1M (peak: 556.3M)
|
||||
CPU: 3.387s
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─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>
|
||||
├─7283 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
├─7285 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
├─7286 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
├─7287 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
├─7288 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
└─7289 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox>
|
||||
|
||||
Jan 26 11:00:00 netbox systemd[1]: Started netbox.service - NetBox WSGI Service.
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible.
|
||||
|
||||
!!! info
|
||||
For the sake of brevity, only Ubuntu 20.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
||||
For the sake of brevity, only Ubuntu 24.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed.
|
||||
|
||||
## Obtain an SSL Certificate
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
The installation instructions provided here have been tested to work on Ubuntu 24.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
1. [Redis](2-redis.md)
|
||||
2. [Redis](2-redis.md)
|
||||
3. [NetBox components](3-netbox.md)
|
||||
4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md)
|
||||
5. [HTTP server](5-http-server.md)
|
||||
|
||||
@@ -65,7 +65,7 @@ Download and extract the latest version:
|
||||
|
||||
```no-highlight
|
||||
# Set $NEWVER to the NetBox version being installed
|
||||
NEWVER=3.5.0
|
||||
NEWVER=4.5.0
|
||||
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
|
||||
sudo tar -xzf v$NEWVER.tar.gz -C /opt
|
||||
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
|
||||
@@ -75,7 +75,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
|
||||
|
||||
```no-highlight
|
||||
# Set $OLDVER to the NetBox version currently installed
|
||||
OLDVER=3.4.9
|
||||
OLDVER=4.4.10
|
||||
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
|
||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
|
||||
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
|
||||
@@ -116,7 +116,7 @@ Check out the desired release by specifying its tag. For example:
|
||||
```
|
||||
cd /opt/netbox && \
|
||||
sudo git fetch --tags && \
|
||||
sudo git checkout v4.2.7
|
||||
sudo git checkout v4.5.0
|
||||
```
|
||||
|
||||
## 4. Run the Upgrade Script
|
||||
@@ -128,7 +128,7 @@ sudo ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||
If the default version of Python is not **at least 3.12**, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example:
|
||||
|
||||
```no-highlight
|
||||
sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh
|
||||
|
||||
@@ -133,23 +133,67 @@ The field "class_type" is an easy way to distinguish what type of object it is w
|
||||
|
||||
## Pagination
|
||||
|
||||
Queries can be paginated by specifying pagination in the query and supplying an offset and optionaly a limit in the query. If no limit is given, a default of 100 is used. Queries are not paginated unless requested in the query. An example paginated query is shown below:
|
||||
The GraphQL API supports two types of pagination. Offset-based pagination operates using an offset relative to the first record in a set, specified by the `offset` parameter. For example, the response to a request specifying an offset of 100 will contain the 101st and later matching records. Offset-based pagination feels very natural, but its performance can suffer when dealing with large data sets due to the overhead involved in calculating the relative offset.
|
||||
|
||||
The alternative approach is cursor-based pagination, which operates using absolute (rather than relative) primary key values. (These are the numeric IDs assigned to each object in the database.) When using cursor-based pagination, the response will contain records with a primary key greater than or equal to the specified start value, up to the maximum number of results. This strategy requires keeping track of the last seen primary key from each response when paginating through data, but is extremely performant. The cursor is specified by passing the starting object ID via the `start` parameter.
|
||||
|
||||
To ensure consistent ordering, objects will always be ordered by their primary keys when cursor-based pagination is used.
|
||||
|
||||
!!! note "Cursor-based pagination was introduced in NetBox v4.5.2."
|
||||
|
||||
Both pagination strategies support passing an optional `limit` parameter. In both approaches, this specifies the maximum number of objects to include in the response. If no limit is specified, a default value of 100 is used.
|
||||
|
||||
### Offset Pagination
|
||||
|
||||
The first page will have an `offset` of zero, or the `offset` parameter will be omitted:
|
||||
|
||||
```
|
||||
query {
|
||||
device_list(pagination: { offset: 0, limit: 20 }) {
|
||||
device_list(pagination: {offset: 0, limit: 20}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The second page will have an offset equal to the size of the first page. If the number of records is less than the specified limit, there are no more records to process. For example, if a request specifies a `limit` of 20 but returns only 13 records, we can conclude that this is the final page of records.
|
||||
|
||||
```
|
||||
query {
|
||||
device_list(pagination: {offset: 20, limit: 20}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor Pagination
|
||||
|
||||
Set the `start` value to zero to fetch the first page. Note that if the `start` parameter is omitted, offset-based pagination will be used by default.
|
||||
|
||||
```
|
||||
query {
|
||||
device_list(pagination: {start: 0, limit: 20}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To determine the `start` value for the next page, add 1 to the primary key (`id`) of the last record in the previous page.
|
||||
|
||||
For example, if the ID of the last record in the previous response was 123, we would specify a `start` value of 124:
|
||||
|
||||
```
|
||||
query {
|
||||
device_list(pagination: {start: 124, limit: 20}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will return up to 20 records with an ID greater than or equal to 124.
|
||||
|
||||
## Authentication
|
||||
|
||||
NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form:
|
||||
|
||||
```
|
||||
Authorization: Token $TOKEN
|
||||
```
|
||||
NetBox's GraphQL API uses the same API authentication tokens as its REST API. See the [REST API authentication](./rest-api.md#authentication) documentation for further detail.
|
||||
|
||||
## Disabling the GraphQL API
|
||||
|
||||
|
||||
@@ -215,9 +215,51 @@ http://netbox/api/ipam/ip-addresses/ \
|
||||
|
||||
If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
|
||||
|
||||
### Brief Format
|
||||
### Specifying Fields
|
||||
|
||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this:
|
||||
A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response.
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?fields=id,name,status,region
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "DM-NYC",
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
"region": {
|
||||
"id": 43,
|
||||
"url": "http://netbox:8000/api/dcim/regions/43/",
|
||||
"display": "New York",
|
||||
"name": "New York",
|
||||
"slug": "us-ny",
|
||||
"description": "",
|
||||
"site_count": 0,
|
||||
"_depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, you can opt to omit only specific fields by passing the `omit` parameter:
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count
|
||||
```
|
||||
|
||||
!!! note "The `omit` parameter was introduced in NetBox v4.5.2."
|
||||
|
||||
Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response.
|
||||
|
||||
!!! note
|
||||
The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence.
|
||||
|
||||
#### Brief Format
|
||||
|
||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this:
|
||||
|
||||
```no-highlight
|
||||
GET /api/ipam/prefixes/13980/
|
||||
@@ -270,10 +312,10 @@ GET /api/ipam/prefixes/13980/
|
||||
}
|
||||
```
|
||||
|
||||
The brief format is much more terse:
|
||||
The brief format includes only a few fields:
|
||||
|
||||
```no-highlight
|
||||
GET /api/ipam/prefixes/13980/?brief=1
|
||||
GET /api/ipam/prefixes/13980/?brief=true
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -610,9 +652,7 @@ http://netbox/api/dcim/sites/ \
|
||||
|
||||
## Changelog Messages
|
||||
|
||||
!!! info "This feature was introduced in NetBox v4.4."
|
||||
|
||||
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
||||
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Additionally, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
||||
|
||||
For example, the following API request will create a new site and record a message in the resulting changelog entry:
|
||||
|
||||
@@ -628,7 +668,7 @@ http://netbox/api/dcim/sites/ \
|
||||
}'
|
||||
```
|
||||
|
||||
This approach works when creating, modifying, or deleting objects, either individually or in bulk.
|
||||
This approach works when creating, modifying, or deleting objects, either individually or in bulk. For more information about change logging, see [Change Logging](../features/change-logging.md).
|
||||
|
||||
## Uploading Files
|
||||
|
||||
|
||||
@@ -21,6 +21,21 @@ The cable's operational status. Choices include:
|
||||
* Planned
|
||||
* Decommissioning
|
||||
|
||||
### Profile
|
||||
|
||||
!!! note "This field was introduced in NetBox v4.5."
|
||||
|
||||
The profile to which the cable conforms. The profile determines the mapping of termination between the two ends and enables logical tracing across complex connections, such as breakout cables. Supported profiles are listed below.
|
||||
|
||||
* Straight (single position)
|
||||
* Straight (multi-position)
|
||||
* Shuffle (2x2 MPO8)
|
||||
* Shuffle (4x4 MPO8)
|
||||
|
||||
A single-position cable is allowed only one termination point at each end. There is no limit to the number of terminations a multi-position cable may have. Each end of a cable must have the same number of terminations, unless connected to a pass-through port or to a circuit termination.
|
||||
|
||||
The assignment of a cable profile is optional. If no profile is assigned, legacy tracing behavior will be preserved.
|
||||
|
||||
### Type
|
||||
|
||||
The cable's physical medium or classification.
|
||||
|
||||
@@ -21,6 +21,13 @@ The VM's operational status.
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Start on boot
|
||||
|
||||
The start on boot setting from the hypervisor.
|
||||
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `VirtualMachine.start_on_boot` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Site & Cluster
|
||||
|
||||
The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned.
|
||||
|
||||
@@ -60,6 +60,13 @@ Four of the standard Python logging levels are supported:
|
||||
|
||||
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
|
||||
|
||||
### Jobs running for Model instances
|
||||
|
||||
A Job can be executed for a specific instance of a Model.
|
||||
To enable this functionality, the model must include the `JobsMixin`.
|
||||
|
||||
When enqueuing a Job, you can associate it with a particular instance by passing that instance to the `instance` parameter.
|
||||
|
||||
### Scheduled Jobs
|
||||
|
||||
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
|
||||
@@ -73,9 +80,10 @@ As described above, jobs can be scheduled for immediate execution or at any late
|
||||
from django.db import models
|
||||
from core.choices import JobIntervalChoices
|
||||
from netbox.models import NetBoxModel
|
||||
from netbox.models.features import JobsMixin
|
||||
from .jobs import MyTestJob
|
||||
|
||||
class MyModel(NetBoxModel):
|
||||
class MyModel(JobsMixin, NetBoxModel):
|
||||
foo = models.CharField()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -6,12 +6,17 @@ Filter sets define the mechanisms available for filtering or searching through a
|
||||
|
||||
To support additional functionality standard to NetBox models, such as tag assignment and custom field support, the `NetBoxModelFilterSet` class is available for use by plugins. This should be used as the base filter set class for plugin models which inherit from `NetBoxModel`. Within this class, individual filters can be declared as directed by the `django-filters` documentation. An example is provided below.
|
||||
|
||||
!!! info "New in NetBox v4.5: FilterSet Registration"
|
||||
NetBox v4.5 introduced the `register_filterset()` utility function. This enables plugins to register their filtersets to receive advanced functionality, such as the automatic attachment of field-specific lookup modifiers on the filter form. Registration is optional: Unregistered filtersets will continue to work as before, but will not receive the enhanced functionality.
|
||||
|
||||
```python
|
||||
# filtersets.py
|
||||
import django_filters
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from utilities.filtersets import register_filterset
|
||||
from .models import MyModel
|
||||
|
||||
@register_filterset
|
||||
class MyFilterSet(NetBoxModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=(
|
||||
@@ -27,6 +32,14 @@ class MyFilterSet(NetBoxModelFilterSet):
|
||||
fields = ('some', 'other', 'fields')
|
||||
```
|
||||
|
||||
In addition to the base NetBoxModelFilterSet class, the following filterset classes are also available for subclasses of standard base models.
|
||||
|
||||
| Model Class | FilterSet Class |
|
||||
|-----------------------|--------------------------------------------------|
|
||||
| `PrimaryModel` | `netbox.filtersets.PrimaryModelFilterSet` |
|
||||
| `OrganizationalModel` | `netbox.filtersets.OrganizationalModelFilterSet` |
|
||||
| `NestedGroupModel` | `netbox.filtersets.NestedGroupModelFilterSet` |
|
||||
|
||||
### Declaring Filter Sets
|
||||
|
||||
To utilize a filter set in a subclass of one of NetBox's generic views (such as `ObjectListView` or `BulkEditView`), define the `filterset` attribute on the view class:
|
||||
@@ -42,7 +55,7 @@ class MyModelListView(ObjectListView):
|
||||
filterset = MyModelFilterSet
|
||||
```
|
||||
|
||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||
|
||||
```python
|
||||
# api/views.py
|
||||
@@ -55,6 +68,29 @@ class MyModelViewSet(...):
|
||||
filterset_class = filtersets.MyModelFilterSet
|
||||
```
|
||||
|
||||
### Implementing Quick Search
|
||||
|
||||
The `ObjectListView` has a field called Quick Search. For Quick Search to work the corresponding FilterSet has to override the `search` method that is implemented in `NetBoxModelFilterSet`. This function takes a queryset and can perform arbitrary operations on it and return it. A common use-case is to search for the given search value in multiple fields:
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyFilterSet(NetBoxModelFilterSet):
|
||||
...
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
```
|
||||
|
||||
The `search` method is also used by the `q` filter in `NetBoxModelFilterSet` which in turn is used by the Search field in the filters tab.
|
||||
|
||||
## Filter Classes
|
||||
|
||||
### TagFilter
|
||||
@@ -69,7 +105,9 @@ This class filters `tags` using the `slug` field. For example:
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag = TagFilter()
|
||||
```
|
||||
@@ -85,7 +123,9 @@ This class filters `tags` using the `id` field. For example:
|
||||
```python
|
||||
from django_filters import FilterSet
|
||||
from extras.filters import TagIDFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
@register_filterset
|
||||
class MyModelFilterSet(FilterSet):
|
||||
tag_id = TagIDFilter()
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Form Classes
|
||||
|
||||
NetBox provides several base form classes for use by plugins.
|
||||
NetBox provides several base form classes for use by plugins. Additional form classes are also available for other standard base model classes (PrimaryModel, OrganizationalModel, and NestedGroupModel).
|
||||
|
||||
| Form Class | Purpose |
|
||||
|----------------------------|--------------------------------------|
|
||||
@@ -19,7 +19,17 @@ This is the base form for creating and editing NetBox models. It extends Django'
|
||||
|-------------|---------------------------------------------------------------------------------------|
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
|
||||
**Example**
|
||||
#### Subclasses
|
||||
|
||||
The corresponding model-specific subclasses of `NetBoxModelForm` are documented below.
|
||||
|
||||
| Model Class | Form Class |
|
||||
|-----------------------|---------------------------|
|
||||
| `PrimaryModel` | `PrimaryModelForm` |
|
||||
| `OrganizationalModel` | `OrganizationalModelForm` |
|
||||
| `NestedGroupModel` | `NestedGroupModelForm` |
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -49,9 +59,19 @@ class MyModelForm(NetBoxModelForm):
|
||||
|
||||
### `NetBoxModelImportForm`
|
||||
|
||||
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for import various types of CSV data, listed below.
|
||||
This form facilitates the bulk import of new objects from CSV, JSON, or YAML data. As with model forms, you'll need to declare a `Meta` subclass specifying the associated `model` and `fields`. NetBox also provides several form fields suitable for importing various types of CSV data, listed [below](#csv-import-fields).
|
||||
|
||||
**Example**
|
||||
#### Subclasses
|
||||
|
||||
The corresponding model-specific subclasses of `NetBoxModelImportForm` are documented below.
|
||||
|
||||
| Model Class | Form Class |
|
||||
|-----------------------|---------------------------------|
|
||||
| `PrimaryModel` | `PrimaryModelImportForm` |
|
||||
| `OrganizationalModel` | `OrganizationalModelImportForm` |
|
||||
| `NestedGroupModel` | `NestedGroupModelImportForm` |
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -83,7 +103,17 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
|
||||
|
||||
**Example**
|
||||
#### Subclasses
|
||||
|
||||
The corresponding model-specific subclasses of `NetBoxModelBulkEditForm` are documented below.
|
||||
|
||||
| Model Class | Form Class |
|
||||
|-----------------------|-----------------------------------|
|
||||
| `PrimaryModel` | `PrimaryModelBulkEditForm` |
|
||||
| `OrganizationalModel` | `OrganizationalModelBulkEditForm` |
|
||||
| `NestedGroupModel` | `NestedGroupModelBulkEditForm` |
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
@@ -125,7 +155,17 @@ This form class is used to render a form expressly for filtering a list of objec
|
||||
| `model` | The model of object being edited |
|
||||
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
|
||||
|
||||
**Example**
|
||||
#### Subclasses
|
||||
|
||||
The corresponding model-specific subclasses of `NetBoxModelFilterSetForm` are documented below.
|
||||
|
||||
| Model Class | Form Class |
|
||||
|-----------------------|------------------------------------|
|
||||
| `PrimaryModel` | `PrimaryModelFilterSetForm` |
|
||||
| `OrganizationalModel` | `OrganizationalModelFilterSetForm` |
|
||||
| `NestedGroupModel` | `NestedGroupModelFilterSetForm` |
|
||||
|
||||
#### Example
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
|
||||
@@ -46,3 +46,19 @@ NetBox provides two object type classes for use by plugins.
|
||||
::: netbox.graphql.types.NetBoxObjectType
|
||||
options:
|
||||
members: false
|
||||
|
||||
## GraphQL Filters
|
||||
|
||||
NetBox provides a base filter class for use by plugins which employ subclasseses of `NetBoxModel`.
|
||||
|
||||
::: netbox.graphql.filters.NetBoxModelFilter
|
||||
options:
|
||||
members: false
|
||||
|
||||
Additionally, the following filter classes are available for subclasses of standard base models.
|
||||
|
||||
| Model Class | FilterSet Class |
|
||||
|-----------------------|----------------------------------------------------|
|
||||
| `PrimaryModel` | `netbox.graphql.filters.PrimaryModelFilter` |
|
||||
| `OrganizationalModel` | `netbox.graphql.filters.OrganizationalModelFilter` |
|
||||
| `NestedGroupModel` | `netbox.graphql.filters.NestedGroupModelFilter` |
|
||||
|
||||
@@ -74,7 +74,7 @@ The plugin source directory contains all the actual Python code and other resour
|
||||
|
||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||
|
||||
```python
|
||||
```python title="__init__.py"
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
class FooBarConfig(PluginConfig):
|
||||
@@ -151,7 +151,7 @@ Any additional apps must be installed within the same Python environment as NetB
|
||||
|
||||
An example `pyproject.toml` is below:
|
||||
|
||||
```
|
||||
```toml title="pyproject.toml"
|
||||
# See PEP 518 for the spec of this file
|
||||
# https://www.python.org/dev/peps/pep-0518/
|
||||
|
||||
@@ -179,11 +179,24 @@ classifiers=[
|
||||
]
|
||||
|
||||
requires-python = ">=3.12.0"
|
||||
|
||||
```
|
||||
|
||||
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
||||
|
||||
## Compatibility Matrix
|
||||
|
||||
Consider adding a file named `COMPATIBILITY.md` to your plugin project root (alongside `pyproject.toml`). This file should contain a table listing the minimum and maximum supported versions of NetBox (`min_version` and `max_version`) for each release. This serves as a handy reference for users who are upgrading from a previous version of your plugin. An example is shown below:
|
||||
|
||||
```markdown title="COMPATIBILITY.md"
|
||||
# Compatibility Matrix
|
||||
|
||||
| Release | Minimum NetBox Version | Maximum NetBox Version |
|
||||
|---------|------------------------|------------------------|
|
||||
| 0.2.0 | 4.4.0 | 4.5.x |
|
||||
| 0.1.1 | 4.3.0 | 4.4.x |
|
||||
| 0.1.0 | 4.3.0 | 4.4.x |
|
||||
```
|
||||
|
||||
## Create a Virtual Environment
|
||||
|
||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with 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/`.)
|
||||
|
||||
@@ -325,14 +325,14 @@ class CircuitTypeType(OrganizationalObjectType):
|
||||
|
||||
### Change filters.py
|
||||
|
||||
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new `autotype_decorator` used to automatically wrap FilterSets to reduce the required code to a minimum.
|
||||
Filter classes should inherit from `netbox.graphql.filters.BaseModelFilter`.
|
||||
|
||||
```python title="New"
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
@@ -340,8 +340,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitFilterSet)
|
||||
class CircuitFilter(BaseFilterMixin):
|
||||
class CircuitFilter(BaseModelFilter):
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
@@ -67,6 +67,46 @@ class MyModel(ExportTemplatesMixin, TagsMixin, models.Model):
|
||||
...
|
||||
```
|
||||
|
||||
### Additional Models
|
||||
|
||||
In addition to the base NetBoxModel class, the following additional classes are provided for convenience.
|
||||
|
||||
!!! info "These model classes were added to the plugins API in NetBox v4.5."
|
||||
|
||||
#### PrimaryModel
|
||||
|
||||
PrimaryModel is the go-to class for most object types. It extends NetBoxModel with `description` and `comments` fields, and it introduces support for ownership assignment.
|
||||
|
||||
| Field | Required | Unique | Description |
|
||||
|---------------|----------|--------|---------------------------------------------|
|
||||
| `owner` | No | No | The object's owner |
|
||||
| `description` | No | No | A human-friendly description for the object |
|
||||
| `comments` | No | No | General comments |
|
||||
|
||||
#### OrganizationalModel
|
||||
|
||||
OrganizationalModel is used by object types whose function is primarily the organization of other objects.
|
||||
|
||||
| Field | Required | Unique | Description |
|
||||
|---------------|----------|--------|---------------------------------------------|
|
||||
| `name` | Yes | Yes | The name of the object |
|
||||
| `slug` | Yes | Yes | A unique URL-friendly identifier |
|
||||
| `owner` | No | No | The object's owner |
|
||||
| `description` | No | No | A human-friendly description for the object |
|
||||
|
||||
#### NestedGroupModel
|
||||
|
||||
NestedGroupModel is used for objects which arrange into a recursive hierarchy (like regions and locations) via its self-referential `parent` foreign key.
|
||||
|
||||
| Field | Required | Unique | Description |
|
||||
|---------------|----------|--------|-----------------------------------------------------------------|
|
||||
| `name` | Yes | Yes | The name of the object |
|
||||
| `slug` | Yes | Yes | A unique URL-friendly identifier |
|
||||
| `parent` | No | No | The object (of the same type) under which this object is nested |
|
||||
| `owner` | No | No | The object's owner |
|
||||
| `description` | No | No | A human-friendly description for the object |
|
||||
| `comments` | No | No | General comments |
|
||||
|
||||
## Database Migrations
|
||||
|
||||
Once you have completed defining the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command. (Ensure that your plugin has been installed and enabled first, otherwise it won't be found.)
|
||||
|
||||
@@ -27,6 +27,14 @@ Serializers are responsible for converting Python objects to JSON data suitable
|
||||
|
||||
The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
|
||||
|
||||
In addition to the base NetBoxModelSerializer class, the following serializer classes are also available for subclasses of standard base models.
|
||||
|
||||
| Model Class | Serializer Class |
|
||||
|-----------------------|--------------------------------------------------------|
|
||||
| `PrimaryModel` | `netbox.api.serializers.PrimaryModelSerializer` |
|
||||
| `OrganizationalModel` | `netbox.api.serializers.OrganizationalModelSerializer` |
|
||||
| `NestedGroupModel` | `netbox.api.serializers.NestedGroupModelSerializer` |
|
||||
|
||||
#### Example
|
||||
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||
|
||||
@@ -36,6 +36,14 @@ class MyModelTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', ...)
|
||||
```
|
||||
|
||||
In addition to the base NetBoxTable class, the following table classes are also available for subclasses of standard base models.
|
||||
|
||||
| Model Class | Table Class |
|
||||
|-----------------------|------------------------------------------|
|
||||
| `PrimaryModel` | `netbox.tables.PrimaryModelTable` |
|
||||
| `OrganizationalModel` | `netbox.tables.OrganizationalModelTable` |
|
||||
| `NestedGroupModel` | `netbox.tables.NestedGroupModelTable` |
|
||||
|
||||
### Table Configuration
|
||||
|
||||
The NetBoxTable class features dynamic configuration to allow users to change their column display and ordering preferences. To configure a table for a specific request, simply call its `configure()` method and pass the current HTTPRequest object. For example:
|
||||
|
||||
148
docs/plugins/development/ui-components.md
Normal file
148
docs/plugins/development/ui-components.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# UI Components
|
||||
|
||||
!!! note "New in NetBox v4.5"
|
||||
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
|
||||
|
||||
!!! danger "Beta Feature"
|
||||
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
|
||||
|
||||
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
||||
|
||||
## Page Layout
|
||||
|
||||
A layout defines the general arrangement of content on a page into rows and columns. The layout is defined under the [view](./views.md) and declares a set of rows, each of which may have one or more columns. Below is an example layout.
|
||||
|
||||
```
|
||||
+-------+-------+-------+
|
||||
| Col 1 | Col 2 | Col 3 |
|
||||
+-------+-------+-------+
|
||||
| Col 4 |
|
||||
+-----------+-----------+
|
||||
| Col 5 | Col 6 |
|
||||
+-----------+-----------+
|
||||
```
|
||||
|
||||
The above layout can be achieved with the following declaration under a view:
|
||||
|
||||
```python
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
|
||||
class MyView(generic.ObjectView):
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
Currently, layouts are supported only for subclasses of [`generic.ObjectView`](./views.md#netbox.views.generic.ObjectView).
|
||||
|
||||
::: netbox.ui.layout.Layout
|
||||
|
||||
::: netbox.ui.layout.SimpleLayout
|
||||
|
||||
::: netbox.ui.layout.Row
|
||||
|
||||
::: netbox.ui.layout.Column
|
||||
|
||||
## Panels
|
||||
|
||||
Within each column, related blocks of content are arranged into panels. Each panel has a title and may have a set of associated actions, but the content within is otherwise arbitrary.
|
||||
|
||||
Plugins can define their own panels by inheriting from the base class `netbox.ui.panels.Panel`. Override the `get_context()` method to pass additional context to your custom panel template. An example is provided below.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.ui.panels import Panel
|
||||
|
||||
class RecentChangesPanel(Panel):
|
||||
template_name = 'my_plugin/panels/recent_changes.html'
|
||||
title = _('Recent Changes')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'changes': get_changes()[:10],
|
||||
}
|
||||
```
|
||||
|
||||
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
|
||||
|
||||
::: netbox.ui.panels.Panel
|
||||
|
||||
::: netbox.ui.panels.ObjectPanel
|
||||
|
||||
::: netbox.ui.panels.ObjectAttributesPanel
|
||||
|
||||
#### Object Attributes
|
||||
|
||||
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
||||
|
||||
| Class | Description |
|
||||
|--------------------------------------|--------------------------------------------------|
|
||||
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
||||
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
||||
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
||||
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
||||
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
||||
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
||||
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
|
||||
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
||||
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
||||
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
||||
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
||||
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
||||
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
||||
|
||||
::: netbox.ui.panels.OrganizationalObjectPanel
|
||||
|
||||
::: netbox.ui.panels.NestedGroupObjectPanel
|
||||
|
||||
::: netbox.ui.panels.CommentsPanel
|
||||
|
||||
::: netbox.ui.panels.JSONPanel
|
||||
|
||||
::: netbox.ui.panels.RelatedObjectsPanel
|
||||
|
||||
::: netbox.ui.panels.ObjectsTablePanel
|
||||
|
||||
::: netbox.ui.panels.TemplatePanel
|
||||
|
||||
::: netbox.ui.panels.PluginContentPanel
|
||||
|
||||
## Panel Actions
|
||||
|
||||
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.ui import actions, panels
|
||||
|
||||
panels.ObjectsTablePanel(
|
||||
model='dcim.Region',
|
||||
title=_('Child Regions'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
::: netbox.ui.actions.PanelAction
|
||||
|
||||
::: netbox.ui.actions.LinkAction
|
||||
|
||||
::: netbox.ui.actions.AddObject
|
||||
|
||||
::: netbox.ui.actions.CopyContent
|
||||
@@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 4.5](./version-4.5.md) (January 2026)
|
||||
|
||||
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
||||
* Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
|
||||
* Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
|
||||
* Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
|
||||
* Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
|
||||
|
||||
#### [Version 4.4](./version-4.4.md) (September 2025)
|
||||
|
||||
* Background Jobs for Bulk Operations ([#19589](https://github.com/netbox-community/netbox/issues/19589), [#19891](https://github.com/netbox-community/netbox/issues/19891))
|
||||
|
||||
@@ -1,5 +1,158 @@
|
||||
# NetBox v4.4
|
||||
|
||||
## v4.4.10 (2026-01-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#20953](https://github.com/netbox-community/netbox/issues/20953) - Show reverse bridge relationships on interface detail pages
|
||||
* [#21071](https://github.com/netbox-community/netbox/issues/21071) - Include request method & URL when displaying server errors
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19506](https://github.com/netbox-community/netbox/issues/19506) - Add filter forms for component templates to ensure object selector support
|
||||
* [#20044](https://github.com/netbox-community/netbox/issues/20044) - Fix dark mode support for rack elevations
|
||||
* [#20320](https://github.com/netbox-community/netbox/issues/20320) - Restore support for selecting related interfaces when bulk editing device interfaces
|
||||
* [#20817](https://github.com/netbox-community/netbox/issues/20817) - Re-enable sync button when disabling scheduled syncing for a data source
|
||||
* [#21045](https://github.com/netbox-community/netbox/issues/21045) - Fix `ValueError` exception when saving a site with an assigned prefix
|
||||
* [#21049](https://github.com/netbox-community/netbox/issues/21049) - Ignore stale custom field data when validating an object
|
||||
* [#21063](https://github.com/netbox-community/netbox/issues/21063) - Check for duplicate choice values when validating a custom field choice set
|
||||
* [#21064](https://github.com/netbox-community/netbox/issues/21064) - Ensures that extra choices in custom field choice sets preserve escaped colons
|
||||
|
||||
---
|
||||
|
||||
## v4.4.9 (2025-12-23)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#20309](https://github.com/netbox-community/netbox/issues/20309) - Support ASDOT notation for ASN ranges
|
||||
* [#20720](https://github.com/netbox-community/netbox/issues/20720) - Add Latvian translations
|
||||
* [#20900](https://github.com/netbox-community/netbox/issues/20900) - Allow filtering custom choice fields by multiple values in the UI
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#17976](https://github.com/netbox-community/netbox/issues/17976) - Remove `devicetype_count` from nested manufacturer to correct OpenAPI schema
|
||||
* [#20011](https://github.com/netbox-community/netbox/issues/20011) - Provide a clear message when encountering duplicate object IDs during bulk import
|
||||
* [#20114](https://github.com/netbox-community/netbox/issues/20114) - Preserve `parent_bay` during device bulk import when tags are present
|
||||
* [#20491](https://github.com/netbox-community/netbox/issues/20491) - Improve handling of numeric ranges in tests
|
||||
* [#20873](https://github.com/netbox-community/netbox/issues/20873) - Fix `AttributeError` exception triggered by event rules associated with an object that supports file attachments
|
||||
* [#20875](https://github.com/netbox-community/netbox/issues/20875) - Ensure that parent object relations are cached (for filtering) on device/module components during instantiation
|
||||
* [#20876](https://github.com/netbox-community/netbox/issues/20876) - Allow editing an IP address that resides within a range marked as populated
|
||||
* [#20912](https://github.com/netbox-community/netbox/issues/20912) - Fix inconsistent clearing of `module` field on ModuleBay
|
||||
* [#20944](https://github.com/netbox-community/netbox/issues/20944) - Ensure cached scope is updated on child objects when a parent region/site/location is changed
|
||||
* [#20948](https://github.com/netbox-community/netbox/issues/20948) - Handle the deletion of related objects with `on_delete=RESTRICT` the same as `CASCADE`
|
||||
* [#20966](https://github.com/netbox-community/netbox/issues/20966) - Fix UI rendering issue when scrolling list of object types in permissions form
|
||||
* [#20969](https://github.com/netbox-community/netbox/issues/20969) - Fix querying of front port templates by `rear_port_id`
|
||||
* [#21011](https://github.com/netbox-community/netbox/issues/21011) - Avoid writing to the database when loading active ConfigRevision
|
||||
* [#21032](https://github.com/netbox-community/netbox/issues/21032) - Avoid SQL subquery in RestrictedQuerySet where unnecessary
|
||||
|
||||
---
|
||||
|
||||
## v4.4.8 (2025-12-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#20068](https://github.com/netbox-community/netbox/issues/20068) - Support the assignment of module type profile attributes via bulk import
|
||||
* [#20914](https://github.com/netbox-community/netbox/issues/20914) - Enable filtering device components by tenant assigned to device
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19918](https://github.com/netbox-community/netbox/issues/19918) - Fix support for `{module}` resolution of components of child modules
|
||||
* [#20759](https://github.com/netbox-community/netbox/issues/20759) - Improve legibility of object types in permissions form
|
||||
* [#20860](https://github.com/netbox-community/netbox/issues/20860) - Ensure user-provided changelog message is recorded when creating device components via the UI
|
||||
* [#20878](https://github.com/netbox-community/netbox/issues/20878) - Use the active database connection when executing custom scripts
|
||||
* [#20888](https://github.com/netbox-community/netbox/issues/20888) - Resolve warnings about non-decimal values for min/max latitude & longitude fields
|
||||
|
||||
---
|
||||
|
||||
## v4.4.7 (2025-11-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#20371](https://github.com/netbox-community/netbox/issues/20371) - Add Molex Micro-Fit 2x3 for power ports & power outlets
|
||||
* [#20731](https://github.com/netbox-community/netbox/issues/20731) - Enable specifying `data_source` & `data_file` when bulk import config templates
|
||||
* [#20820](https://github.com/netbox-community/netbox/issues/20820) - Enable filtering of custom fields by object type
|
||||
* [#20823](https://github.com/netbox-community/netbox/issues/20823) - Disallow creation of API tokens with an expiration date in the past
|
||||
* [#20841](https://github.com/netbox-community/netbox/issues/20841) - Support advanced filtering for available rack types when creating/editing a rack
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20134](https://github.com/netbox-community/netbox/issues/20134) - Prevent out-of-band HTMX content swaps in embedded tables
|
||||
* [#20432](https://github.com/netbox-community/netbox/issues/20432) - Fix tracing of cables across multiple circuits in parallel
|
||||
* [#20465](https://github.com/netbox-community/netbox/issues/20465) - Ensure that scripts are updated immediately when a new file is uploaded
|
||||
* [#20638](https://github.com/netbox-community/netbox/issues/20638) - Correct OpenAPI schema for bulk create operations
|
||||
* [#20649](https://github.com/netbox-community/netbox/issues/20649) - Enforce view permissions on REST API endpoint for custom scripts
|
||||
* [#20740](https://github.com/netbox-community/netbox/issues/20740) - Ensure permissions constraints are enforced when executing custom scripts via the REST API
|
||||
* [#20743](https://github.com/netbox-community/netbox/issues/20743) - Pass request context to custom script when triggered by an event rule
|
||||
* [#20766](https://github.com/netbox-community/netbox/issues/20766) - Fix inadvertent translations on server error page
|
||||
* [#20775](https://github.com/netbox-community/netbox/issues/20775) - Fix `TypeError` exception when bulk renaming unnamed devices
|
||||
* [#20822](https://github.com/netbox-community/netbox/issues/20822) - Add missing `auto_sync_enabled` field in bulk edit forms
|
||||
* [#20827](https://github.com/netbox-community/netbox/issues/20827) - Fix UI styling issue when toggling between light and dark mode
|
||||
* [#20839](https://github.com/netbox-community/netbox/issues/20839) - Fix filtering by object type in UI for custom links and saved filters
|
||||
* [#20840](https://github.com/netbox-community/netbox/issues/20840) - Remove extraneous references to airflow for RackType model
|
||||
* [#20844](https://github.com/netbox-community/netbox/issues/20844) - Fix object type filter for L2VPN terminations
|
||||
* [#20859](https://github.com/netbox-community/netbox/issues/20859) - Prevent dashboard crash due to exception raised by a widget
|
||||
* [#20865](https://github.com/netbox-community/netbox/issues/20865) - Enforce proper min/max values for latitude & longitude fields
|
||||
|
||||
---
|
||||
|
||||
## v4.4.6 (2025-11-11)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14171](https://github.com/netbox-community/netbox/issues/14171) - Support VLAN assignment for device & VM interfaces being bulk imported
|
||||
* [#20297](https://github.com/netbox-community/netbox/issues/20297) - Introduce additional coaxial cable types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20378](https://github.com/netbox-community/netbox/issues/20378) - Prevent exception when attempting to delete a data source utilized by a custom script
|
||||
* [#20645](https://github.com/netbox-community/netbox/issues/20645) - CSVChoiceField should defer to model field's default value when CSV field is empty
|
||||
* [#20647](https://github.com/netbox-community/netbox/issues/20647) - Improve handling of empty strings during bulk imports
|
||||
* [#20653](https://github.com/netbox-community/netbox/issues/20653) - Fix filtering of jobs by object type ID
|
||||
* [#20660](https://github.com/netbox-community/netbox/issues/20660) - Optimize loading of custom script modules from remote storage
|
||||
* [#20670](https://github.com/netbox-community/netbox/issues/20670) - Improve validation of related objects during bulk import
|
||||
* [#20688](https://github.com/netbox-community/netbox/issues/20688) - Suppress non-harmful "No active configuration revision found" warning message
|
||||
* [#20697](https://github.com/netbox-community/netbox/issues/20697) - Prevent duplication of signals which increment/decrement related object counts
|
||||
* [#20699](https://github.com/netbox-community/netbox/issues/20699) - Ensure proper ordering of changelog entries resulting from cascading deletions
|
||||
* [#20713](https://github.com/netbox-community/netbox/issues/20713) - Ensure a pre-change snapshot is recorded on virtual chassis members being added/removed
|
||||
* [#20721](https://github.com/netbox-community/netbox/issues/20721) - Fix breadcrumb navigation links in UI for background tasks
|
||||
* [#20738](https://github.com/netbox-community/netbox/issues/20738) - Deleting a virtual chassis should nullify the `vc_position` of all former members
|
||||
* [#20750](https://github.com/netbox-community/netbox/issues/20750) - Fix cloning of permissions when only one action is enabled
|
||||
* [#20755](https://github.com/netbox-community/netbox/issues/20755) - Prevent duplicate results under certain conditions when filtering providers
|
||||
* [#20771](https://github.com/netbox-community/netbox/issues/20771) - Comments are required when creating a new journal entry
|
||||
* [#20774](https://github.com/netbox-community/netbox/issues/20774) - Bulk action button labels should be translated
|
||||
|
||||
---
|
||||
|
||||
## v4.4.5 (2025-10-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#19751](https://github.com/netbox-community/netbox/issues/19751) - Disable occupied module bays in form dropdowns when installing a new module
|
||||
* [#20301](https://github.com/netbox-community/netbox/issues/20301) - Add a "dismiss all" option to the notifications dropdown
|
||||
* [#20399](https://github.com/netbox-community/netbox/issues/20399) - Add `assigned` and `primary` boolean filters for MAC addresses
|
||||
* [#20567](https://github.com/netbox-community/netbox/issues/20567) - Add contacts column to services table
|
||||
* [#20675](https://github.com/netbox-community/netbox/issues/20675) - Enable [NetBox Copilot](https://netboxlabs.com/products/netbox-copilot/) integration
|
||||
* [#20692](https://github.com/netbox-community/netbox/issues/20692) - Add contacts column to IP addresses table
|
||||
* [#20700](https://github.com/netbox-community/netbox/issues/20700) - Add contacts table column for various additional models
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19872](https://github.com/netbox-community/netbox/issues/19872) - Ensure custom script validation failures display error messages
|
||||
* [#20389](https://github.com/netbox-community/netbox/issues/20389) - Fix "select all" behavior for bulk rename views
|
||||
* [#20422](https://github.com/netbox-community/netbox/issues/20422) - Enable filtering of aggregates and prefixes by family in GraphQL API
|
||||
* [#20459](https://github.com/netbox-community/netbox/issues/20459) - Fix validation of `is_oob` & `is_primary` fields under IP address bulk import
|
||||
* [#20466](https://github.com/netbox-community/netbox/issues/20466) - Fix querying of devices with a primary IP assigned in GraphQL API
|
||||
* [#20498](https://github.com/netbox-community/netbox/issues/20498) - Enforce the validation regex (if set) for custom URL fields
|
||||
* [#20524](https://github.com/netbox-community/netbox/issues/20524) - Raise a validation error when attempting to schedule a custom script for a past date/time
|
||||
* [#20541](https://github.com/netbox-community/netbox/issues/20541) - Fix resolution of GraphQL object fields which rely on custom filters
|
||||
* [#20551](https://github.com/netbox-community/netbox/issues/20551) - Fix automatic slug generation in quick-add UI form
|
||||
* [#20606](https://github.com/netbox-community/netbox/issues/20606) - Enable copying of values from table columns rendered as badges
|
||||
* [#20641](https://github.com/netbox-community/netbox/issues/20641) - Fix `AttributeError` exception raised by the object changes REST API endpoint
|
||||
* [#20646](https://github.com/netbox-community/netbox/issues/20646) - Prevent cables from connecting to objects marked as connected
|
||||
* [#20655](https://github.com/netbox-community/netbox/issues/20655) - Fix `FieldError` exception when attempting to sort permissions list by actions
|
||||
|
||||
---
|
||||
|
||||
## v4.4.4 (2025-10-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
235
docs/release-notes/version-4.5.md
Normal file
235
docs/release-notes/version-4.5.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# NetBox v4.5
|
||||
|
||||
## v4.5.2 (2026-02-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#15801](https://github.com/netbox-community/netbox/issues/15801) - Add link peer and connection columns to the VLAN device interfaces table
|
||||
* [#19221](https://github.com/netbox-community/netbox/issues/19221) - Truncate long image attachment filenames in the UI
|
||||
* [#19869](https://github.com/netbox-community/netbox/issues/19869) - Display peer connections for LAG member interfaces
|
||||
* [#20052](https://github.com/netbox-community/netbox/issues/20052) - Increase logging level of error message when a custom script fails to load
|
||||
* [#20172](https://github.com/netbox-community/netbox/issues/20172) - Add `cabled` filter for interfaces in GraphQL API
|
||||
* [#21081](https://github.com/netbox-community/netbox/issues/21081) - Add owner group table columns & filters across all supported object list views
|
||||
* [#21088](https://github.com/netbox-community/netbox/issues/21088) - Add max depth and max length dropdowns for child prefix views
|
||||
* [#21110](https://github.com/netbox-community/netbox/issues/21110) - Support cursor-based pagination in GraphQL API
|
||||
* [#21201](https://github.com/netbox-community/netbox/issues/21201) - Pre-populate GenericForeignKey form fields when cloning
|
||||
* [#21209](https://github.com/netbox-community/netbox/issues/21209) - Ignore case sensitivity for configuration parameters which specify an app label and model name
|
||||
* [#21228](https://github.com/netbox-community/netbox/issues/21228) - Support image attachments for rack types
|
||||
* [#21244](https://github.com/netbox-community/netbox/issues/21244) - Enable omitting specific fields from REST API responses with `?omit=` parameter
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* [#21249](https://github.com/netbox-community/netbox/issues/21249) - Avoid extraneous user query when no event rules are present
|
||||
* [#21259](https://github.com/netbox-community/netbox/issues/21259) - Cache ObjectType lookups for the duration of a request
|
||||
* [#21260](https://github.com/netbox-community/netbox/issues/21260) - Defer object serialization for events pipeline processing
|
||||
* [#21263](https://github.com/netbox-community/netbox/issues/21263) - Prefetch related objects after creating/updating objects via REST API
|
||||
* [#21300](https://github.com/netbox-community/netbox/issues/21300) - Cache custom field lookups for the duration of a request
|
||||
* [#21302](https://github.com/netbox-community/netbox/issues/21302) - Avoid redundant uniqueness checks in ValidatedModelSerializer
|
||||
* [#21303](https://github.com/netbox-community/netbox/issues/21303) - Cache post-change snapshot on each instance after serialization
|
||||
* [#21327](https://github.com/netbox-community/netbox/issues/21327) - Always leverage `get_by_natural_key()` to resolve ContentTypes
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20212](https://github.com/netbox-community/netbox/issues/20212) - Fix support for image attachment thumbnails when using S3 storage
|
||||
* [#20383](https://github.com/netbox-community/netbox/issues/20383) - When editing a device, clearing the assigned unit should also clear the rack face selection
|
||||
* [#20902](https://github.com/netbox-community/netbox/issues/20902) - Avoid `SyncError` exception when Git URL contains an embedded username
|
||||
* [#20977](https://github.com/netbox-community/netbox/issues/20977) - "Run again" button should respect script variable defaults
|
||||
* [#21115](https://github.com/netbox-community/netbox/issues/21115) - Include `attribute_data` in ModuleType YAML export
|
||||
* [#21129](https://github.com/netbox-community/netbox/issues/21129) - Store queue name on the Job model to ensure deletion of associated RQ task when a non-default queue is used
|
||||
* [#21168](https://github.com/netbox-community/netbox/issues/21168) - Fix Application Service cloning to preserve parent object
|
||||
* [#21173](https://github.com/netbox-community/netbox/issues/21173) - Ensure all plugin menu items are registered regardless of initialization order
|
||||
* [#21176](https://github.com/netbox-community/netbox/issues/21176) - Remove checkboxes from IP ranges in mixed-type tables
|
||||
* [#21202](https://github.com/netbox-community/netbox/issues/21202) - Fix scoped form cloning clearing the `scope` field when `scope_type` changes
|
||||
* [#21214](https://github.com/netbox-community/netbox/issues/21214) - Clean up AutoSyncRecord when detaching from DataSource
|
||||
* [#21242](https://github.com/netbox-community/netbox/issues/21242) - Navigation menu items for authentication should not require `staff_only` permission
|
||||
* [#21254](https://github.com/netbox-community/netbox/issues/21254) - Fix `AttributeError` exception when checking for latest release
|
||||
* [#21262](https://github.com/netbox-community/netbox/issues/21262) - Assigned scope should be replicated when cloning a prefix
|
||||
* [#21269](https://github.com/netbox-community/netbox/issues/21269) - Fix replication of front/rear port assignments from the module type when installing a module
|
||||
|
||||
---
|
||||
|
||||
## v4.5.1 (2026-01-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#21018](https://github.com/netbox-community/netbox/issues/21018) - Enable filtering prefixes by location/site/site group/region directly via GraphQL API
|
||||
* [#21142](https://github.com/netbox-community/netbox/issues/21142) - Enable filtering device components by site/location/rack directly via GraphQL API
|
||||
* [#21144](https://github.com/netbox-community/netbox/issues/21144) - Enable specifying a prefix length for IP addresses when utilizing the `/api/ipam/prefixes/<id>/available-ips/` REST API endpoint
|
||||
* [#21165](https://github.com/netbox-community/netbox/issues/21165) - VLAN selector should default to group (instead of site)
|
||||
* [#21178](https://github.com/netbox-community/netbox/issues/21178) - Improve consistency of rack measurements in UI
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#19901](https://github.com/netbox-community/netbox/issues/19901) - Fix `RelatedObjectDoesNotExist` exception when importing modules into unnamed devices
|
||||
* [#20239](https://github.com/netbox-community/netbox/issues/20239) - Prevent shared mutable state in PluginMenuItem & PluginMenuButton
|
||||
* [#20933](https://github.com/netbox-community/netbox/issues/20933) - Fix writable `data_file` assignment for ConfigContext and ConfigContextProfile via the REST API
|
||||
* [#21039](https://github.com/netbox-community/netbox/issues/21039) - Fix support for AVIF image uploads
|
||||
* [#21050](https://github.com/netbox-community/netbox/issues/21050) - Clear device OOB IP assignments when reassigning IP addresses
|
||||
* [#21051](https://github.com/netbox-community/netbox/issues/21051) - Remove irrelevant object types from permissions form
|
||||
* [#21097](https://github.com/netbox-community/netbox/issues/21097) - Fix comparison lookups for ID filters in GraphQL API
|
||||
* [#21102](https://github.com/netbox-community/netbox/issues/21102) - Fix GraphiQL explorer UI
|
||||
* [#21117](https://github.com/netbox-community/netbox/issues/21117) - Avoid `ValueError` exception when `API_TOKEN_PEPPERS` is not defined
|
||||
* [#21118](https://github.com/netbox-community/netbox/issues/21118) - Address performance issue when saving sites with many assigned objects
|
||||
* [#21124](https://github.com/netbox-community/netbox/issues/21124) - Fix front/rear port mapping for module types
|
||||
* [#21134](https://github.com/netbox-community/netbox/issues/21134) - Fix bulk renaming for module types
|
||||
* [#21139](https://github.com/netbox-community/netbox/issues/21139) - Support `fields` parameter for job, object change, and object type REST API endpoints
|
||||
* [#21140](https://github.com/netbox-community/netbox/issues/21140) - Restore translation for object attribute labels on several UI views
|
||||
* [#21160](https://github.com/netbox-community/netbox/issues/21160) - Fix performance issue loading UI views caused by unintended `APISelect` choices resolution
|
||||
* [#21166](https://github.com/netbox-community/netbox/issues/21166) - Fix support for 32-bit ASN filtering in GraphQL API
|
||||
* [#21175](https://github.com/netbox-community/netbox/issues/21175) - Fix pending migrations warning when `DEFAULT_LANGUAGE` is set
|
||||
* [#21181](https://github.com/netbox-community/netbox/issues/21181) - Handle `AuthenticationFailed` exception when using an invalid API token to fetch media files
|
||||
* [#21213](https://github.com/netbox-community/netbox/issues/21213) - Tag weight field should be marked as required in UI forms
|
||||
* [#21231](https://github.com/netbox-community/netbox/issues/21231) - Presence of object types table should be checked only during migration (performance improvement)
|
||||
|
||||
---
|
||||
|
||||
## v4.5.0 (2026-01-06)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12, 3.13, or 3.14.
|
||||
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. For example, `id: 123` becomes `id: {exact: 123 }`.
|
||||
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
|
||||
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
|
||||
* API tokens can no longer be reassigned from one user to another.
|
||||
* A config context assigned to a platform will now also apply to any children of that platform. (Although this is typically desired behavior, it may introduce unanticipated changes for existing deployments.)
|
||||
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly via the `/api/dcim/cables/` endpoint.
|
||||
* The UI view dedicated to swapping A/Z circuit terminations has been removed.
|
||||
* The experimental HTMX navigation feature has been removed.
|
||||
* The obsolete boolean field `is_staff` has been removed from the `User` model.
|
||||
* Removal of deprecated behavior
|
||||
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
|
||||
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
|
||||
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
|
||||
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
||||
|
||||
Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter. Plugins can register their own filtersets using the `register_filterset()` decorator to enable this new functionality.
|
||||
|
||||
(Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.)
|
||||
|
||||
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
|
||||
|
||||
This release introduces a new version of API token (v2) which implements several security improvements. HMAC hashing with a cryptographic pepper is used to authenticate these tokens, obviating the need to store plaintexts. The new tokens also employ a non-sensitive key which can be shared to identify tokens without divulging their plaintexts. We've also adopted the standard "bearer" HTTP header format, as shown below.
|
||||
|
||||
```
|
||||
# v1 token header
|
||||
Authorization: Token <TOKEN>
|
||||
|
||||
# v2 token header
|
||||
Authorization: Bearer nbt_<KEY>.<TOKEN>
|
||||
```
|
||||
|
||||
Note that v2 token keys are prefixed with the fixed string `nbt_`, which can be used to aid in secret detection.
|
||||
|
||||
Backward compatibility with legacy (v1) tokens is retained in this release. However, users are strongly encouraged to begin using only v2 tokens, as support for legacy tokens will be removed in NetBox v4.7.
|
||||
|
||||
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
|
||||
|
||||
An optional `owner` foreign key field has been added to most models. This enables the assignment of objects to a new Owner model, which represents a set of users and/or groups. Through this relationship, we can now convey ownership of objects within NetBox natively, without needing to rely on the assignment of tags or custom fields.
|
||||
|
||||
(Note that ownership differs significantly in function from tenancy. Ownership determines the parties responsible for the maintenance of an object, whereas as tenancy conveys an operational dependency.)
|
||||
|
||||
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
|
||||
|
||||
The previous many-to-one mapping of front to rear ports has been expanded to support bidirectional mappings. The `rear_port` and `rear_port_position` fields on the FrontPort model have been replaced with an intermediary PortMapping model, which supports any number of assignments between front port/position pair and a rear port/position pair. This change unlocks the ability to model complex inline devices that swap individual fiber pairs between cables.
|
||||
|
||||
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
|
||||
|
||||
Cables can now be assigned profiles which determine how they are treated for path tracing. A profile indicates the number of discrete parallel channels or lanes carried by the cable among its endpoints. For example, a 1-to-4 breakout cable has four lanes, shared at one end via a common termination and split out at the other end to four separate terminations. Profiles, when assigned, enable NetBox to more accurately trace a specific connection within a cable, rather than the cable as a whole.
|
||||
|
||||
The assignment of cable profiles is optional: Cable tracing will continue to operate as before for cables with no profile assigned.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is now required to render a device or virtual machine configuration
|
||||
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
|
||||
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
|
||||
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
|
||||
* [#19523](https://github.com/netbox-community/netbox/issues/19523) - Cache the number of instances for device, module, and rack types, and enable filtering by these counts
|
||||
* [#20417](https://github.com/netbox-community/netbox/issues/20417) - Add an optional `color` field for device type power outlets
|
||||
* [#20476](https://github.com/netbox-community/netbox/issues/20476) - Once provisioned, the owner of an API token cannot be changed
|
||||
* [#20492](https://github.com/netbox-community/netbox/issues/20492) - Completely disabled the means to retrieve legacy API token plaintexts (removed the `ALLOW_TOKEN_RETRIEVAL` config parameter)
|
||||
* [#20639](https://github.com/netbox-community/netbox/issues/20639) - Apply config contexts to devices/VMs assigned any child platform of the parent platform
|
||||
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
|
||||
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
|
||||
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
|
||||
* [#20929](https://github.com/netbox-community/netbox/issues/20929) - Require the `render_config` permission to view a rendered device/VM configuration in the UI
|
||||
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
|
||||
* [#20959](https://github.com/netbox-community/netbox/issues/20959) - Include a count of related module types for a manufacturer in the REST API
|
||||
|
||||
### Plugins
|
||||
|
||||
* [#13182](https://github.com/netbox-community/netbox/issues/13182) - Added `PrimaryModel`, `OrganizationalModel`, and `NestedGroupModel` to the plugins API, as well as their respective base classes for various resources
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#16137](https://github.com/netbox-community/netbox/issues/16137) - Remove the obsolete boolean field `is_staff` from the `User` model
|
||||
* [#17571](https://github.com/netbox-community/netbox/issues/17571) - Remove the experimental HTMX navigation feature
|
||||
* [#17936](https://github.com/netbox-community/netbox/issues/17936) - Introduce a dedicated `GFKSerializerField` for representing generic foreign keys in API serializers
|
||||
* [#19889](https://github.com/netbox-community/netbox/issues/19889) - Drop support for Python 3.10 and 3.11
|
||||
* [#19898](https://github.com/netbox-community/netbox/issues/19898) - Remove the obsolete REST API endpoint `/api/extras/object-types/`
|
||||
* [#20088](https://github.com/netbox-community/netbox/issues/20088) - Remove the non-deterministic `model` key from webhook payload data
|
||||
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
|
||||
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
|
||||
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
|
||||
* [#20295](https://github.com/netbox-community/netbox/issues/20295) - Cable terminations may be modified via the REST API only by modifying the cable itself
|
||||
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
|
||||
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swapping A/Z circuit terminations
|
||||
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Most objects now include an optional `owner` foreign key field.
|
||||
* The `/api/dcim/cable-terminations` endpoint is now read-only.
|
||||
* Introduced the `/api/authentication-check/` endpoint to test REST API credentials
|
||||
* `circuits.CircuitGroup`
|
||||
* Add optional `comments` field
|
||||
* `circuits.CircuitType`
|
||||
* Add optional `comments` field
|
||||
* `circuits.VirtualCircuitType`
|
||||
* Add optional `comments` field
|
||||
* `dcim.Cable`
|
||||
* Add the optional `profile` choice field
|
||||
* `dcim.FrontPort`
|
||||
* Removed the `rear_port` and `rear_port_position` fields
|
||||
* Add the `positions` integer field
|
||||
* Add the `rear_ports` list for port mappings
|
||||
* `dcim.InventoryItemRole`
|
||||
* Add optional `comments` field
|
||||
* `dcim.Manufacturer`
|
||||
* Add optional `comments` field
|
||||
* Add read-only `moduletype_count` integer field
|
||||
* `dcim.ModuleType`
|
||||
* Add read-only `module_count` integer field
|
||||
* `dcim.PowerOutletTemplate`
|
||||
* Add optional `color` field
|
||||
* `dcim.RackRole`
|
||||
* Add optional `comments` field
|
||||
* `dcim.RackType`
|
||||
* Add read-only `rack_count` integer field
|
||||
* `dcim.RearPort`
|
||||
* Add the `front_ports` list for port mappings
|
||||
* `ipam.ASNRange`
|
||||
* Add optional `comments` field
|
||||
* `ipam.RIR`
|
||||
* Add optional `comments` field
|
||||
* `ipam.Role`
|
||||
* Add optional `comments` field
|
||||
* `ipam.VLANGroup`
|
||||
* Add optional `comments` field
|
||||
* `tenancy.ContactRole`
|
||||
* Add optional `comments` field
|
||||
* `users.Token`
|
||||
* Add `enabled` boolean field
|
||||
* `virtualization.ClusterGroup`
|
||||
* Add optional `comments` field
|
||||
* `virtualization.ClusterType`
|
||||
* Add optional `comments` field
|
||||
* `virtualization.VirtualMachine`
|
||||
* Add optional `start_on_boot` choice field
|
||||
* `vpn.TunnelGroup`
|
||||
* Add optional `comments` field
|
||||
@@ -143,6 +143,7 @@ nav:
|
||||
- Getting Started: 'plugins/development/index.md'
|
||||
- Models: 'plugins/development/models.md'
|
||||
- Views: 'plugins/development/views.md'
|
||||
- UI Components: 'plugins/development/ui-components.md'
|
||||
- Navigation: 'plugins/development/navigation.md'
|
||||
- Templates: 'plugins/development/templates.md'
|
||||
- Tables: 'plugins/development/tables.md'
|
||||
@@ -321,6 +322,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.5: 'release-notes/version-4.5.md'
|
||||
- Version 4.4: 'release-notes/version-4.4.md'
|
||||
- Version 4.3: 'release-notes/version-4.3.md'
|
||||
- Version 4.2: 'release-notes/version-4.2.md'
|
||||
|
||||
@@ -25,10 +25,12 @@ from extras.models import Bookmark
|
||||
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
from users import forms
|
||||
from users.models import UserConfig
|
||||
from users.tables import TokenTable
|
||||
from users.ui.panels import TokenExamplePanel, TokenPanel
|
||||
from utilities.request import safe_for_redirect
|
||||
from utilities.string import remove_linebreaks
|
||||
from utilities.views import register_model_view
|
||||
@@ -342,12 +344,21 @@ class UserTokenListView(LoginRequiredMixin, View):
|
||||
|
||||
@register_model_view(UserToken)
|
||||
class UserTokenView(LoginRequiredMixin, View):
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
TokenPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TokenExamplePanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get(self, request, pk):
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
|
||||
return render(request, 'account/token.html', {
|
||||
'object': token,
|
||||
'layout': self.layout,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
@@ -11,12 +10,12 @@ from circuits.models import (
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import (
|
||||
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
__all__ = (
|
||||
@@ -39,7 +38,7 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
@@ -55,7 +54,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
termination = GFKSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@@ -64,14 +63,6 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
'upstream_speed', 'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupSerializer(OrganizationalModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@@ -80,7 +71,7 @@ class CircuitGroupSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
@@ -134,7 +125,7 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
termination = GFKSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@@ -146,20 +137,12 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
member_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
|
||||
)
|
||||
member = serializers.SerializerMethodField(read_only=True)
|
||||
member = GFKSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
@@ -169,14 +152,6 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_member(self, obj):
|
||||
if obj.member_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.member)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.member, nested=True, context=context).data
|
||||
|
||||
|
||||
class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
|
||||
@@ -186,7 +161,7 @@ class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
||||
|
||||
@@ -11,6 +11,7 @@ from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -29,6 +30,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -89,12 +91,11 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(accounts__account__icontains=value) |
|
||||
Q(accounts__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -122,6 +123,7 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -149,6 +151,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -156,6 +159,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -267,6 +271,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -348,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||
'mark_connected', 'pp_info', 'cable_end',
|
||||
'mark_connected', 'pp_info', 'cable_end', 'cable_connector',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -362,6 +367,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -369,6 +375,7 @@ class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -468,6 +475,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -475,6 +483,7 @@ class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
@@ -531,6 +540,7 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
).distinct()
|
||||
|
||||
|
||||
@register_filterset
|
||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -99,7 +99,7 @@ class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class CircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||
@@ -241,7 +241,7 @@ class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
|
||||
model = CircuitGroup
|
||||
nullable_fields = (
|
||||
'description', 'tenant',
|
||||
'description', 'tenant', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -73,7 +73,7 @@ class CircuitTypeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class CircuitImportForm(PrimaryModelImportForm):
|
||||
@@ -176,7 +176,7 @@ class CircuitGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
@@ -199,7 +199,7 @@ class VirtualCircuitTypeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class VirtualCircuitImportForm(PrimaryModelImportForm):
|
||||
|
||||
@@ -34,9 +34,10 @@ __all__ = (
|
||||
class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('asn_id', name=_('ASN')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -69,8 +70,9 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'account', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
@@ -88,8 +90,9 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor
|
||||
class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'service_id', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
@@ -107,8 +110,9 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
|
||||
class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = CircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -121,7 +125,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = Circuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet(
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
@@ -129,6 +133,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelF
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
||||
@@ -274,8 +279,9 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -312,8 +318,9 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -326,10 +333,11 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
|
||||
@@ -97,7 +97,7 @@ class CircuitTypeForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ class VirtualCircuitTypeForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -3,17 +3,18 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin
|
||||
from strawberry_django import BaseFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.enums import ColorEnum
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitTypeFilterMixin',
|
||||
'CircuitTypeFilterMixin',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
class CircuitTypeFilterMixin:
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -4,20 +4,15 @@ from typing import Annotated, TYPE_CHECKING
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import FilterLookup, DateFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
||||
|
||||
from circuits import models
|
||||
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filter_mixins import (
|
||||
DistanceFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
OrganizationalModelFilterMixin,
|
||||
PrimaryModelFilterMixin,
|
||||
)
|
||||
from netbox.graphql.filter_mixins import DistanceFilterMixin, ImageAttachmentFilterMixin
|
||||
from netbox.graphql.filters import ChangeLoggedModelFilter, OrganizationalModelFilter, PrimaryModelFilter
|
||||
from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from .filter_mixins import BaseCircuitTypeFilterMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
@@ -43,16 +38,17 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
|
||||
class CircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
TagsFilterMixin,
|
||||
ChangeLogFilterMixin,
|
||||
ChangeLoggedModelFilter,
|
||||
CabledObjectModelFilterMixin,
|
||||
):
|
||||
circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
term_side: Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
|
||||
term_side: (
|
||||
BaseFilterLookup[Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')]] | None
|
||||
) = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
termination_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
@@ -93,7 +89,7 @@ class CircuitFilter(
|
||||
ImageAttachmentFilterMixin,
|
||||
DistanceFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilterMixin
|
||||
PrimaryModelFilter
|
||||
):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -108,7 +104,7 @@ class CircuitFilter(
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
type_id: ID | None = strawberry_django.filter_field()
|
||||
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
|
||||
status: BaseFilterLookup[Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
install_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
|
||||
@@ -122,19 +118,17 @@ class CircuitFilter(
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitType, lookups=True)
|
||||
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class CircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
|
||||
class CircuitGroupAssignmentFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -143,13 +137,13 @@ class CircuitGroupAssignmentFilter(
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group_id: ID | None = strawberry_django.filter_field()
|
||||
priority: Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
|
||||
priority: BaseFilterLookup[Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -159,7 +153,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -169,7 +163,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
class ProviderNetworkFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -179,12 +173,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -198,7 +192,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
type_id: ID | None = strawberry_django.filter_field()
|
||||
status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
|
||||
status: BaseFilterLookup[Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group_assignments: Annotated['CircuitGroupAssignmentFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -207,14 +201,16 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
|
||||
class VirtualCircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
virtual_circuit_id: ID | None = strawberry_django.filter_field()
|
||||
role: Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')] | None = (
|
||||
role: (
|
||||
BaseFilterLookup[
|
||||
Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')]
|
||||
] | None
|
||||
) = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
interface: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
|
||||
39
netbox/circuits/migrations/0054_cable_connector_positions.py
Normal file
39
netbox/circuits/migrations/0054_cable_connector_positions.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0053_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0054_cable_connector_positions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuitgroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualcircuittype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
17
netbox/circuits/migrations/0056_gfk_indexes.py
Normal file
17
netbox/circuits/migrations/0056_gfk_indexes.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0055_add_comments_to_organizationalmodel'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0224_add_comments_to_organizationalmodel'),
|
||||
('extras', '0134_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='circuittermination',
|
||||
index=models.Index(fields=['termination_type', 'termination_id'], name='circuits_ci_termina_505dda_idx'),
|
||||
),
|
||||
]
|
||||
@@ -335,6 +335,9 @@ class CircuitTermination(
|
||||
name='%(app_label)s_%(class)s_unique_circuit_term_side'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('termination_type', 'termination_id')),
|
||||
)
|
||||
verbose_name = _('circuit termination')
|
||||
verbose_name_plural = _('circuit terminations')
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class CircuitGroupIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -44,6 +45,7 @@ class CircuitTypeIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -109,5 +111,6 @@ class VirtualCircuitTypeIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -40,8 +40,8 @@ class CircuitTypeTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = CircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'comments', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
||||
|
||||
@@ -175,7 +175,7 @@ class CircuitGroupTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = CircuitGroup
|
||||
fields = (
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'comments', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||
|
||||
@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilterSet
|
||||
ignore_fields = ('cable',)
|
||||
ignore_fields = ('cable', 'cable_positions')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -78,6 +78,7 @@ class ProviderBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
|
||||
class ProviderBulkRenameView(generic.BulkRenameView):
|
||||
queryset = Provider.objects.all()
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -145,6 +146,7 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
|
||||
class ProviderAccountBulkRenameView(generic.BulkRenameView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -221,6 +223,7 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
|
||||
class ProviderNetworkBulkRenameView(generic.BulkRenameView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -285,6 +288,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
|
||||
class CircuitTypeBulkRenameView(generic.BulkRenameView):
|
||||
queryset = CircuitType.objects.all()
|
||||
filterset = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -357,6 +361,7 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
class CircuitBulkRenameView(generic.BulkRenameView):
|
||||
queryset = Circuit.objects.all()
|
||||
field_name = 'cid'
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -476,6 +481,7 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
|
||||
class CircuitGroupBulkRenameView(generic.BulkRenameView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -591,6 +597,7 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
|
||||
class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
|
||||
@@ -663,6 +670,7 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
|
||||
class VirtualCircuitBulkRenameView(generic.BulkRenameView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
field_name = 'cid'
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'bulk_delete', path='delete', detail=False)
|
||||
|
||||
@@ -12,6 +12,7 @@ from drf_spectacular.utils import Direction
|
||||
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
|
||||
@@ -49,6 +50,11 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
)
|
||||
|
||||
|
||||
def viewset_handles_bulk_create(view):
|
||||
"""Check if view automatically provides list-based bulk create"""
|
||||
return isinstance(view, NetBoxModelViewSet)
|
||||
|
||||
|
||||
class NetBoxAutoSchema(AutoSchema):
|
||||
"""
|
||||
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
||||
@@ -128,6 +134,36 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
|
||||
return response_serializers
|
||||
|
||||
def _get_request_for_media_type(self, serializer, direction='request'):
|
||||
"""
|
||||
Override to generate oneOf schema for serializers that support both
|
||||
single object and array input (NetBoxModelViewSet POST operations).
|
||||
|
||||
Refs: #20638
|
||||
"""
|
||||
# Get the standard schema first
|
||||
schema, required = super()._get_request_for_media_type(serializer, direction)
|
||||
|
||||
# If this serializer supports arrays (marked in get_request_serializer),
|
||||
# wrap the schema in oneOf to allow single object OR array
|
||||
if (
|
||||
direction == 'request' and
|
||||
schema is not None and
|
||||
getattr(self.view, 'action', None) == 'create' and
|
||||
viewset_handles_bulk_create(self.view)
|
||||
):
|
||||
return {
|
||||
'oneOf': [
|
||||
schema, # Single object
|
||||
{
|
||||
'type': 'array',
|
||||
'items': schema, # Array of objects
|
||||
}
|
||||
]
|
||||
}, required
|
||||
|
||||
return schema, required
|
||||
|
||||
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
|
||||
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import ObjectChange
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
__all__ = (
|
||||
'ObjectChangeSerializer',
|
||||
@@ -26,7 +24,10 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
changed_object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
changed_object = serializers.SerializerMethodField(
|
||||
changed_object = GFKSerializerField(
|
||||
read_only=True
|
||||
)
|
||||
object_repr = serializers.CharField(
|
||||
read_only=True
|
||||
)
|
||||
prechange_data = serializers.JSONField(
|
||||
@@ -44,22 +45,6 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
||||
'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data',
|
||||
'postchange_data',
|
||||
'changed_object_type', 'changed_object_id', 'changed_object', 'object_repr', 'message',
|
||||
'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_changed_object(self, obj):
|
||||
"""
|
||||
Serialize a nested representation of the changed object.
|
||||
"""
|
||||
if obj.changed_object is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
serializer = get_serializer_for_model(obj.changed_object)
|
||||
except SerializerNotFound:
|
||||
return obj.object_repr
|
||||
data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data
|
||||
|
||||
return data
|
||||
|
||||
@@ -44,3 +44,4 @@ class DataFileSerializer(NetBoxModelSerializer):
|
||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'path')
|
||||
read_only_fields = ['path', 'last_updated', 'size', 'hash']
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import Job
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
__all__ = (
|
||||
'JobSerializer',
|
||||
@@ -18,11 +23,29 @@ class JobSerializer(BaseModelSerializer):
|
||||
object_type = ContentTypeField(
|
||||
read_only=True
|
||||
)
|
||||
object = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
|
||||
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
|
||||
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
|
||||
'log_entries',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_object(self, obj):
|
||||
"""
|
||||
Serialize a nested representation of the object.
|
||||
"""
|
||||
if obj.object is None:
|
||||
return None
|
||||
try:
|
||||
serializer = get_serializer_for_model(obj.object)
|
||||
except SerializerNotFound:
|
||||
return obj.object_repr
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.object, nested=True, context=context).data
|
||||
|
||||
@@ -11,7 +11,6 @@ from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.worker import Worker
|
||||
|
||||
@@ -64,7 +63,7 @@ class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
||||
filterset_class = filtersets.DataFileFilterSet
|
||||
|
||||
|
||||
class JobViewSet(ReadOnlyModelViewSet):
|
||||
class JobViewSet(NetBoxReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of job results
|
||||
"""
|
||||
@@ -73,19 +72,20 @@ class JobViewSet(ReadOnlyModelViewSet):
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
class ObjectChangeViewSet(NetBoxReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.all()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
return ObjectChange.objects.valid_models()
|
||||
return super().get_queryset().valid_models()
|
||||
|
||||
|
||||
class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
||||
class ObjectTypeViewSet(NetBoxReadOnlyModelViewSet):
|
||||
"""
|
||||
Read-only list of ObjectTypes.
|
||||
"""
|
||||
@@ -94,6 +94,16 @@ class ObjectTypeViewSet(ReadOnlyModelViewSet):
|
||||
serializer_class = serializers.ObjectTypeSerializer
|
||||
filterset_class = filtersets.ObjectTypeFilterSet
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Override initial() to skip the restrict() call since ObjectType (a ContentType proxy)
|
||||
doesn't use RestrictedQuerySet and is publicly accessible metadata.
|
||||
"""
|
||||
# Call GenericViewSet.initial() directly, skipping BaseViewSet.initial()
|
||||
# which would try to call restrict() on the queryset
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
GenericViewSet.initial(self, request, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseRQViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
|
||||
@@ -21,11 +21,24 @@ __all__ = (
|
||||
'GitBackend',
|
||||
'LocalBackend',
|
||||
'S3Backend',
|
||||
'url_has_embedded_credentials',
|
||||
)
|
||||
|
||||
logger = logging.getLogger('netbox.data_backends')
|
||||
|
||||
|
||||
def url_has_embedded_credentials(url):
|
||||
"""
|
||||
Check if a URL contains embedded credentials (username in the URL).
|
||||
|
||||
URLs like 'https://user@bitbucket.org/...' have embedded credentials.
|
||||
This is used to avoid passing explicit credentials to dulwich when the
|
||||
URL already contains them, which would cause authentication conflicts.
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
return bool(parsed.username)
|
||||
|
||||
|
||||
@register_data_backend()
|
||||
class LocalBackend(DataBackend):
|
||||
name = 'local'
|
||||
@@ -102,7 +115,9 @@ class GitBackend(DataBackend):
|
||||
clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy)
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
if self.params.get('username'):
|
||||
# Only pass explicit credentials if URL doesn't already contain embedded username
|
||||
# to avoid credential conflicts (see #20902)
|
||||
if not url_has_embedded_credentials(self.url) and self.params.get('username'):
|
||||
clone_args.update(
|
||||
{
|
||||
"username": self.params.get('username'),
|
||||
|
||||
@@ -7,6 +7,7 @@ from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, Primary
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -20,6 +21,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=get_data_backend_choices,
|
||||
@@ -48,6 +50,7 @@ class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search'
|
||||
@@ -75,11 +78,16 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class JobFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
field_name='object_type_id',
|
||||
)
|
||||
object_type = ContentTypeFilter()
|
||||
created = django_filters.DateTimeFilter()
|
||||
created__before = django_filters.DateTimeFilter(
|
||||
@@ -121,10 +129,14 @@ class JobFilterSet(BaseFilterSet):
|
||||
choices=JobStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
queue_name = django_filters.CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
|
||||
fields = (
|
||||
'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
|
||||
'queue_name',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -135,6 +147,7 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ObjectTypeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -160,6 +173,7 @@ class ObjectTypeFilterSet(BaseFilterSet):
|
||||
return queryset.filter(features__icontains=value)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -199,6 +213,7 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -26,8 +26,9 @@ __all__ = (
|
||||
class DataSourceFilterForm(PrimaryModelFilterSetForm):
|
||||
model = DataSource
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
@@ -71,13 +72,13 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Job
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'status', name=_('Attributes')),
|
||||
FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
|
||||
FieldSet(
|
||||
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
|
||||
),
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
object_type_id = ContentTypeChoiceField(
|
||||
label=_('Object Type'),
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
required=False,
|
||||
@@ -87,6 +88,10 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=JobStatusChoices,
|
||||
required=False
|
||||
)
|
||||
queue_name = forms.CharField(
|
||||
label=_('Queue'),
|
||||
required=False
|
||||
)
|
||||
created__after = forms.DateTimeField(
|
||||
label=_('Created after'),
|
||||
required=False,
|
||||
|
||||
@@ -166,8 +166,8 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
|
||||
FieldSet('CUSTOM_VALIDATORS', 'PROTECTION_RULES', name=_('Validation')),
|
||||
FieldSet('DEFAULT_USER_PREFERENCES', name=_('User Preferences')),
|
||||
FieldSet(
|
||||
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
|
||||
name=_('Miscellaneous')
|
||||
'MAINTENANCE_MODE', 'COPILOT_ENABLED', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION',
|
||||
'MAPS_URL', name=_('Miscellaneous'),
|
||||
),
|
||||
FieldSet('comment', name=_('Config Revision'))
|
||||
)
|
||||
|
||||
11
netbox/core/graphql/enums.py
Normal file
11
netbox/core/graphql/enums.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import strawberry
|
||||
|
||||
from core.choices import *
|
||||
|
||||
__all__ = (
|
||||
'DataSourceStatusEnum',
|
||||
'ObjectChangeActionEnum',
|
||||
)
|
||||
|
||||
DataSourceStatusEnum = strawberry.enum(DataSourceStatusChoices.as_enum(prefix='status'))
|
||||
ObjectChangeActionEnum = strawberry.enum(ObjectChangeActionChoices.as_enum(prefix='action'))
|
||||
@@ -4,31 +4,19 @@ from typing import Annotated, TYPE_CHECKING
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import DatetimeFilterLookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterMixin',
|
||||
'BaseObjectTypeFilterMixin',
|
||||
'ChangeLogFilterMixin',
|
||||
'ChangeLoggingMixin',
|
||||
)
|
||||
|
||||
|
||||
# @strawberry.input
|
||||
class BaseFilterMixin: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseObjectTypeFilterMixin(BaseFilterMixin):
|
||||
id: ID | None = strawberry.UNSET
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeLogFilterMixin(BaseFilterMixin):
|
||||
id: ID | None = strawberry.UNSET
|
||||
class ChangeLoggingMixin:
|
||||
# TODO: "changelog" is not a valid field name; needs to be updated for ObjectChange
|
||||
changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -5,11 +5,11 @@ import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType as DjangoContentType
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core import models
|
||||
from core.graphql.filter_mixins import BaseFilterMixin
|
||||
from netbox.graphql.filter_mixins import PrimaryModelFilterMixin
|
||||
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
from .enums import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import IntegerLookup, JSONFilter
|
||||
@@ -24,8 +24,7 @@ __all__ = (
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DataFile, lookups=True)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
class DataFileFilter(BaseModelFilter):
|
||||
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
@@ -40,11 +39,13 @@ class DataFileFilter(BaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
class DataSourceFilter(PrimaryModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: (
|
||||
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None
|
||||
) = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -57,13 +58,14 @@ class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
class ObjectChangeFilter(BaseModelFilter):
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action: (
|
||||
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
|
||||
) = strawberry_django.filter_field()
|
||||
changed_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -83,7 +85,6 @@ class ObjectChangeFilter(BaseFilterMixin):
|
||||
|
||||
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
class ContentTypeFilter(BaseModelFilter):
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
18
netbox/core/migrations/0021_job_queue_name.py
Normal file
18
netbox/core/migrations/0021_job_queue_name.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-27 00:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='queue_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -63,16 +63,20 @@ class ConfigRevision(models.Model):
|
||||
return reverse('core:config') # Default config view
|
||||
return reverse('core:configrevision', args=[self.pk])
|
||||
|
||||
def activate(self):
|
||||
def activate(self, update_db=True):
|
||||
"""
|
||||
Cache the configuration data.
|
||||
|
||||
Parameters:
|
||||
update_db: Mark the ConfigRevision as active in the database (default: True)
|
||||
"""
|
||||
cache.set('config', self.data, None)
|
||||
cache.set('config_version', self.pk, None)
|
||||
|
||||
# Set all instances of ConfigRevision to false and set this instance to true
|
||||
ConfigRevision.objects.all().update(active=False)
|
||||
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
|
||||
if update_db:
|
||||
# Set all instances of ConfigRevision to false and set this instance to true
|
||||
ConfigRevision.objects.all().update(active=False)
|
||||
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
|
||||
|
||||
activate.alters_data = True
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
from netbox.models import PrimaryModel
|
||||
@@ -128,9 +128,24 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
# Ensure URL scheme matches selected type
|
||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||
raise ValidationError({
|
||||
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
|
||||
scheme='file://'
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# If recurring sync is disabled for an existing DataSource, clear any pending sync jobs for it and reset its
|
||||
# "queued" status
|
||||
if not self._state.adding and not self.sync_interval:
|
||||
self.jobs.filter(status=JobStatusChoices.STATUS_PENDING).delete()
|
||||
if self.status == DataSourceStatusChoices.QUEUED and self.last_synced:
|
||||
self.status = DataSourceStatusChoices.COMPLETED
|
||||
elif self.status == DataSourceStatusChoices.QUEUED:
|
||||
self.status = DataSourceStatusChoices.NEW
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.core.files.storage import storages
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from ..choices import ManagedFileRootPathChoices
|
||||
@@ -64,9 +63,6 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('core:managedfile', args=[self.pk])
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.file_path
|
||||
|
||||
@@ -112,6 +112,12 @@ class Job(models.Model):
|
||||
verbose_name=_('job ID'),
|
||||
unique=True
|
||||
)
|
||||
queue_name = models.CharField(
|
||||
verbose_name=_('queue name'),
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_('Name of the queue in which this job was enqueued')
|
||||
)
|
||||
log_entries = ArrayField(
|
||||
verbose_name=_('log entries'),
|
||||
base_field=models.JSONField(
|
||||
@@ -179,11 +185,15 @@ class Job(models.Model):
|
||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
|
||||
rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
rq_job_id = str(self.job_id)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
# Cancel the RQ job using the stored queue name
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
job = queue.fetch_job(str(self.job_id))
|
||||
job = queue.fetch_job(rq_job_id)
|
||||
|
||||
if job:
|
||||
try:
|
||||
@@ -288,7 +298,8 @@ class Job(models.Model):
|
||||
scheduled=schedule_at,
|
||||
interval=interval,
|
||||
user=user,
|
||||
job_id=uuid.uuid4()
|
||||
job_id=uuid.uuid4(),
|
||||
queue_name=rq_queue_name
|
||||
)
|
||||
job.full_clean()
|
||||
job.save()
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db import connection, models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.context import query_cache
|
||||
from netbox.plugins import PluginConfig
|
||||
from netbox.registry import registry
|
||||
from utilities.string import title
|
||||
@@ -35,6 +36,10 @@ class ObjectTypeQuerySet(models.QuerySet):
|
||||
|
||||
class ObjectTypeManager(models.Manager):
|
||||
|
||||
# TODO: Remove this in NetBox v5.0
|
||||
# Cache the result of introspection to avoid repeated queries.
|
||||
_table_exists = False
|
||||
|
||||
def get_queryset(self):
|
||||
return ObjectTypeQuerySet(self.model, using=self._db)
|
||||
|
||||
@@ -66,13 +71,21 @@ class ObjectTypeManager(models.Manager):
|
||||
"""
|
||||
from netbox.models.features import get_model_features, model_is_public
|
||||
|
||||
# Check the request cache before hitting the database
|
||||
cache = query_cache.get()
|
||||
if cache is not None:
|
||||
if ot := cache['object_types'].get((model._meta.model, for_concrete_model)):
|
||||
return ot
|
||||
|
||||
# TODO: Remove this in NetBox v5.0
|
||||
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
||||
# fall back to ContentType.
|
||||
if 'core_objecttype' not in connection.introspection.table_names():
|
||||
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
||||
ct.features = get_model_features(ct.model_class())
|
||||
return ct
|
||||
if not ObjectTypeManager._table_exists:
|
||||
if 'core_objecttype' not in connection.introspection.table_names():
|
||||
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
||||
ct.features = get_model_features(ct.model_class())
|
||||
return ct
|
||||
ObjectTypeManager._table_exists = True
|
||||
|
||||
if not inspect.isclass(model):
|
||||
model = model.__class__
|
||||
@@ -90,6 +103,10 @@ class ObjectTypeManager(models.Manager):
|
||||
features=get_model_features(model),
|
||||
)[0]
|
||||
|
||||
# Populate the request cache to avoid redundant lookups
|
||||
if cache is not None:
|
||||
cache['object_types'][(model._meta.model, for_concrete_model)] = ot
|
||||
|
||||
return ot
|
||||
|
||||
def get_for_models(self, *models, for_concrete_models=True):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.object_actions import ObjectAction
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from threading import local
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.models import CASCADE, RESTRICT
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
||||
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
@@ -17,6 +18,7 @@ from extras.events import enqueue_event
|
||||
from extras.models import Tag
|
||||
from extras.utils import run_validators
|
||||
from netbox.config import get_config
|
||||
from utilities.data import get_config_value_ci
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
|
||||
from utilities.exceptions import AbortRequest
|
||||
@@ -167,7 +169,7 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
# to queueing any events for the object being deleted, in case a validation error is
|
||||
# raised, causing the deletion to fail.
|
||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
||||
validators = get_config_value_ci(get_config().PROTECTION_RULES, model_name, default=[])
|
||||
try:
|
||||
run_validators(instance, validators)
|
||||
except ValidationError as e:
|
||||
@@ -220,14 +222,8 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
if type(relation) is ManyToManyRel:
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
elif type(relation) is ManyToOneRel and relation.field.null is True:
|
||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
||||
setattr(obj, related_field_name, None)
|
||||
# make sure the object hasn't been deleted - in case of
|
||||
# deletion chaining of related objects
|
||||
try:
|
||||
obj.refresh_from_db()
|
||||
except DoesNotExist:
|
||||
continue
|
||||
obj.save()
|
||||
|
||||
# Enqueue the object for event processing
|
||||
|
||||
@@ -42,6 +42,9 @@ class JobTable(NetBoxTable):
|
||||
completed = columns.DateTimeColumn(
|
||||
verbose_name=_('Completed'),
|
||||
)
|
||||
queue_name = tables.Column(
|
||||
verbose_name=_('Queue'),
|
||||
)
|
||||
log_entries = tables.Column(
|
||||
verbose_name=_('Log Entries'),
|
||||
)
|
||||
@@ -53,7 +56,7 @@ class JobTable(NetBoxTable):
|
||||
model = Job
|
||||
fields = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
|
||||
'completed', 'user', 'error', 'job_id',
|
||||
'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
|
||||
|
||||
@@ -5,14 +5,16 @@ from rest_framework import status
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import ObjectChange, ObjectType
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site, CableTermination, Device, DeviceType, DeviceRole, Interface, Cable
|
||||
from dcim.choices import InterfaceTypeChoices, ModuleStatusChoices, SiteStatusChoices
|
||||
from dcim.models import (
|
||||
Cable, CableTermination, Device, DeviceRole, DeviceType, Manufacturer, Module, ModuleBay, ModuleType, Interface,
|
||||
Site,
|
||||
)
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoiceSet, Tag
|
||||
from utilities.testing import APITestCase
|
||||
from utilities.testing.utils import create_tags, post_data
|
||||
from utilities.testing.utils import create_tags, create_test_device, post_data
|
||||
from utilities.testing.views import ModelViewTestCase
|
||||
from dcim.models import Manufacturer
|
||||
|
||||
|
||||
class ChangeLogViewTest(ModelViewTestCase):
|
||||
@@ -622,3 +624,64 @@ class ChangeLogAPITest(APITestCase):
|
||||
self.assertEqual(objectchange.prechange_data['name'], 'Site 1')
|
||||
self.assertEqual(objectchange.prechange_data['slug'], 'site-1')
|
||||
self.assertEqual(objectchange.postchange_data, None)
|
||||
|
||||
def test_deletion_ordering(self):
|
||||
"""
|
||||
Check that the cascading deletion of dependent objects is recorded in the correct order.
|
||||
"""
|
||||
device = create_test_device('device1')
|
||||
module_bay = ModuleBay.objects.create(device=device, name='Module Bay 1')
|
||||
module_type = ModuleType.objects.create(manufacturer=Manufacturer.objects.first(), model='Module Type 1')
|
||||
self.add_permissions('dcim.add_module', 'dcim.add_interface', 'dcim.delete_module')
|
||||
self.assertEqual(ObjectChange.objects.count(), 0) # Sanity check
|
||||
|
||||
# Create a new Module
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module_bay': module_bay.pk,
|
||||
'module_type': module_type.pk,
|
||||
'status': ModuleStatusChoices.STATUS_ACTIVE,
|
||||
}
|
||||
url = reverse('dcim-api:module-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
module = device.modules.first()
|
||||
|
||||
# Create an Interface on the Module
|
||||
data = {
|
||||
'device': device.pk,
|
||||
'module': module.pk,
|
||||
'name': 'Interface 1',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
}
|
||||
url = reverse('dcim-api:interface-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
interface = device.interfaces.first()
|
||||
|
||||
# Delete the Module
|
||||
url = reverse('dcim-api:module-detail', kwargs={'pk': module.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(Module.objects.count(), 0)
|
||||
self.assertEqual(Interface.objects.count(), 0)
|
||||
|
||||
# Verify the creation of the expected ObjectChange records. We should see four total records, in this order:
|
||||
# 1. Module created
|
||||
# 2. Interface created
|
||||
# 3. Interface deleted
|
||||
# 4. Module deleted
|
||||
changes = ObjectChange.objects.order_by('time')
|
||||
self.assertEqual(len(changes), 4)
|
||||
self.assertEqual(changes[0].changed_object_type, ContentType.objects.get_for_model(Module))
|
||||
self.assertEqual(changes[0].changed_object_id, module.pk)
|
||||
self.assertEqual(changes[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface))
|
||||
self.assertEqual(changes[1].changed_object_id, interface.pk)
|
||||
self.assertEqual(changes[1].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Interface))
|
||||
self.assertEqual(changes[2].changed_object_id, interface.pk)
|
||||
self.assertEqual(changes[2].action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
self.assertEqual(changes[3].changed_object_type, ContentType.objects.get_for_model(Module))
|
||||
self.assertEqual(changes[3].changed_object_id, module.pk)
|
||||
self.assertEqual(changes[3].action, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
|
||||
116
netbox/core/tests/test_data_backends.py
Normal file
116
netbox/core/tests/test_data_backends.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from unittest import skipIf
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from core.data_backends import url_has_embedded_credentials
|
||||
|
||||
try:
|
||||
import dulwich # noqa: F401
|
||||
DULWICH_AVAILABLE = True
|
||||
except ImportError:
|
||||
DULWICH_AVAILABLE = False
|
||||
|
||||
|
||||
class URLEmbeddedCredentialsTests(TestCase):
|
||||
def test_url_with_embedded_username(self):
|
||||
self.assertTrue(url_has_embedded_credentials('https://myuser@bitbucket.org/workspace/repo.git'))
|
||||
|
||||
def test_url_without_embedded_username(self):
|
||||
self.assertFalse(url_has_embedded_credentials('https://bitbucket.org/workspace/repo.git'))
|
||||
|
||||
def test_url_with_username_and_password(self):
|
||||
self.assertTrue(url_has_embedded_credentials('https://user:pass@bitbucket.org/workspace/repo.git'))
|
||||
|
||||
def test_various_providers_with_embedded_username(self):
|
||||
urls = [
|
||||
'https://user@bitbucket.org/workspace/repo.git',
|
||||
'https://user@github.com/owner/repo.git',
|
||||
'https://deploy-key@gitlab.com/group/project.git',
|
||||
'http://user@internal-git.example.com/repo.git',
|
||||
]
|
||||
for url in urls:
|
||||
with self.subTest(url=url):
|
||||
self.assertTrue(url_has_embedded_credentials(url))
|
||||
|
||||
def test_various_providers_without_embedded_username(self):
|
||||
"""Various Git providers without embedded usernames."""
|
||||
urls = [
|
||||
'https://bitbucket.org/workspace/repo.git',
|
||||
'https://github.com/owner/repo.git',
|
||||
'https://gitlab.com/group/project.git',
|
||||
'http://internal-git.example.com/repo.git',
|
||||
]
|
||||
for url in urls:
|
||||
with self.subTest(url=url):
|
||||
self.assertFalse(url_has_embedded_credentials(url))
|
||||
|
||||
def test_ssh_url(self):
|
||||
# git@host:path format doesn't parse as having a username in the traditional sense
|
||||
self.assertFalse(url_has_embedded_credentials('git@github.com:owner/repo.git'))
|
||||
|
||||
def test_file_url(self):
|
||||
self.assertFalse(url_has_embedded_credentials('file:///path/to/repo'))
|
||||
|
||||
|
||||
@skipIf(not DULWICH_AVAILABLE, "dulwich is not installed")
|
||||
class GitBackendCredentialIntegrationTests(TestCase):
|
||||
"""
|
||||
Integration tests that verify GitBackend correctly applies credential logic.
|
||||
|
||||
These tests require dulwich to be installed and verify the full integration
|
||||
of the credential handling in GitBackend.fetch().
|
||||
"""
|
||||
|
||||
def _get_clone_kwargs(self, url, **params):
|
||||
from core.data_backends import GitBackend
|
||||
|
||||
backend = GitBackend(url=url, **params)
|
||||
|
||||
with patch('dulwich.porcelain.clone') as mock_clone, \
|
||||
patch('dulwich.porcelain.NoneStream'):
|
||||
try:
|
||||
with backend.fetch():
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_clone.called:
|
||||
return mock_clone.call_args.kwargs
|
||||
return {}
|
||||
|
||||
def test_url_with_embedded_username_skips_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_url_without_embedded_username_passes_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), 'myuser')
|
||||
self.assertEqual(kwargs.get('password'), 'my-api-key')
|
||||
|
||||
def test_url_with_embedded_username_no_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_public_repo_no_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://github.com/public/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
@@ -1,8 +1,10 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource, ObjectType
|
||||
from core.models import DataSource, Job, ObjectType
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from dcim.models import Site, Location, Device
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
@@ -200,3 +202,38 @@ class ObjectTypeTest(TestCase):
|
||||
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
|
||||
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
|
||||
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
|
||||
|
||||
|
||||
class JobTest(TestCase):
|
||||
|
||||
@patch('core.models.jobs.django_rq.get_queue')
|
||||
def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
|
||||
"""
|
||||
Test that when a job is deleted, it's canceled from the correct queue.
|
||||
"""
|
||||
mock_queue = MagicMock()
|
||||
mock_rq_job = MagicMock()
|
||||
mock_queue.fetch_job.return_value = mock_rq_job
|
||||
mock_get_queue.return_value = mock_queue
|
||||
|
||||
def dummy_func(**kwargs):
|
||||
pass
|
||||
|
||||
# Enqueue a job with a custom queue name
|
||||
custom_queue = 'my_custom_queue'
|
||||
job = Job.enqueue(
|
||||
func=dummy_func,
|
||||
name='Test Job',
|
||||
queue_name=custom_queue
|
||||
)
|
||||
|
||||
# Reset mock to clear enqueue call
|
||||
mock_get_queue.reset_mock()
|
||||
|
||||
# Delete the job
|
||||
job.delete()
|
||||
|
||||
# Verify the correct queue was used for cancellation
|
||||
mock_get_queue.assert_called_with(custom_queue)
|
||||
mock_queue.fetch_job.assert_called_with(str(job.job_id))
|
||||
mock_rq_job.cancel.assert_called_once()
|
||||
|
||||
108
netbox/core/tests/test_openapi_schema.py
Normal file
108
netbox/core/tests/test_openapi_schema.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Unit tests for OpenAPI schema generation.
|
||||
|
||||
Refs: #20638
|
||||
"""
|
||||
import json
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class OpenAPISchemaTestCase(TestCase):
|
||||
"""Tests for OpenAPI schema generation."""
|
||||
|
||||
def setUp(self):
|
||||
"""Fetch schema via API endpoint."""
|
||||
response = self.client.get('/api/schema/', {'format': 'json'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.schema = json.loads(response.content)
|
||||
|
||||
def test_post_operation_documents_single_or_array(self):
|
||||
"""
|
||||
POST operations on NetBoxModelViewSet endpoints should document
|
||||
support for both single objects and arrays via oneOf.
|
||||
|
||||
Refs: #20638
|
||||
"""
|
||||
# Test representative endpoints across different apps
|
||||
test_paths = [
|
||||
'/api/core/data-sources/',
|
||||
'/api/dcim/sites/',
|
||||
'/api/users/users/',
|
||||
'/api/ipam/ip-addresses/',
|
||||
]
|
||||
|
||||
for path in test_paths:
|
||||
with self.subTest(path=path):
|
||||
operation = self.schema['paths'][path]['post']
|
||||
|
||||
# Get the request body schema
|
||||
request_schema = operation['requestBody']['content']['application/json']['schema']
|
||||
|
||||
# Should have oneOf with two options
|
||||
self.assertIn('oneOf', request_schema, f"POST {path} should have oneOf schema")
|
||||
self.assertEqual(
|
||||
len(request_schema['oneOf']), 2,
|
||||
f"POST {path} oneOf should have exactly 2 options"
|
||||
)
|
||||
|
||||
# First option: single object (has $ref or properties)
|
||||
single_schema = request_schema['oneOf'][0]
|
||||
self.assertTrue(
|
||||
'$ref' in single_schema or 'properties' in single_schema,
|
||||
f"POST {path} first oneOf option should be single object"
|
||||
)
|
||||
|
||||
# Second option: array of objects
|
||||
array_schema = request_schema['oneOf'][1]
|
||||
self.assertEqual(
|
||||
array_schema['type'], 'array',
|
||||
f"POST {path} second oneOf option should be array"
|
||||
)
|
||||
self.assertIn('items', array_schema, f"POST {path} array should have items")
|
||||
|
||||
def test_bulk_update_operations_require_array_only(self):
|
||||
"""
|
||||
Bulk update/patch operations should require arrays only, not oneOf.
|
||||
They don't support single object input.
|
||||
|
||||
Refs: #20638
|
||||
"""
|
||||
test_paths = [
|
||||
'/api/dcim/sites/',
|
||||
'/api/users/users/',
|
||||
]
|
||||
|
||||
for path in test_paths:
|
||||
for method in ['put', 'patch']:
|
||||
with self.subTest(path=path, method=method):
|
||||
operation = self.schema['paths'][path][method]
|
||||
request_schema = operation['requestBody']['content']['application/json']['schema']
|
||||
|
||||
# Should be array-only, not oneOf
|
||||
self.assertNotIn(
|
||||
'oneOf', request_schema,
|
||||
f"{method.upper()} {path} should NOT have oneOf (array-only)"
|
||||
)
|
||||
self.assertEqual(
|
||||
request_schema['type'], 'array',
|
||||
f"{method.upper()} {path} should require array"
|
||||
)
|
||||
self.assertIn(
|
||||
'items', request_schema,
|
||||
f"{method.upper()} {path} array should have items"
|
||||
)
|
||||
|
||||
def test_bulk_delete_requires_array(self):
|
||||
"""
|
||||
Bulk delete operations should require arrays.
|
||||
|
||||
Refs: #20638
|
||||
"""
|
||||
path = '/api/dcim/sites/'
|
||||
operation = self.schema['paths'][path]['delete']
|
||||
request_schema = operation['requestBody']['content']['application/json']['schema']
|
||||
|
||||
# Should be array-only
|
||||
self.assertNotIn('oneOf', request_schema, "DELETE should NOT have oneOf")
|
||||
self.assertEqual(request_schema['type'], 'array', "DELETE should require array")
|
||||
self.assertIn('items', request_schema, "DELETE array should have items")
|
||||
@@ -125,6 +125,7 @@ class DataSourceBulkEditView(generic.BulkEditView):
|
||||
@register_model_view(DataSource, 'bulk_rename', path='rename', detail=False)
|
||||
class DataSourceBulkRenameView(generic.BulkRenameView):
|
||||
queryset = DataSource.objects.all()
|
||||
filterset = filtersets.DataSourceFilterSet
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
|
||||
|
||||
@@ -2,10 +2,12 @@ from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
__all__ = (
|
||||
'ConnectedEndpointsSerializer',
|
||||
'PortSerializer',
|
||||
)
|
||||
|
||||
|
||||
@@ -35,3 +37,53 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
def get_connected_endpoints_reachable(self, obj):
|
||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||
|
||||
|
||||
class PortSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Base serializer for front & rear port and port templates.
|
||||
"""
|
||||
@property
|
||||
def _mapper(self):
|
||||
"""
|
||||
Return the model and ForeignKey field name used to track port mappings for this model.
|
||||
"""
|
||||
if self.Meta.model is FrontPort:
|
||||
return PortMapping, 'front_port'
|
||||
if self.Meta.model is RearPort:
|
||||
return PortMapping, 'rear_port'
|
||||
if self.Meta.model is FrontPortTemplate:
|
||||
return PortTemplateMapping, 'front_port'
|
||||
if self.Meta.model is RearPortTemplate:
|
||||
return PortTemplateMapping, 'rear_port'
|
||||
raise ValueError(f"Could not determine mapping details for {self.__class__}")
|
||||
|
||||
def create(self, validated_data):
|
||||
mappings = validated_data.pop('mappings', [])
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Create port mappings
|
||||
mapping_model, fk_name = self._mapper
|
||||
for attrs in mappings:
|
||||
mapping_model.objects.create(**{
|
||||
fk_name: instance,
|
||||
**attrs,
|
||||
})
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
mappings = validated_data.pop('mappings', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if mappings is not None:
|
||||
# Update port mappings
|
||||
mapping_model, fk_name = self._mapper
|
||||
mapping_model.objects.filter(**{fk_name: instance}).delete()
|
||||
for attrs in mappings:
|
||||
mapping_model.objects.create(**{
|
||||
fk_name: instance,
|
||||
**attrs,
|
||||
})
|
||||
|
||||
return instance
|
||||
|
||||
@@ -5,6 +5,7 @@ from rest_framework import serializers
|
||||
from dcim.choices import *
|
||||
from dcim.models import Cable, CablePath, CableTermination
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.gfk_fields import GFKSerializerField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer, PrimaryModelSerializer,
|
||||
)
|
||||
@@ -24,15 +25,16 @@ class CableSerializer(PrimaryModelSerializer):
|
||||
a_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||
profile = ChoiceField(choices=CableProfileChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant',
|
||||
'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'profile',
|
||||
'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'label', 'description')
|
||||
|
||||
@@ -53,24 +55,19 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
termination_type = ContentTypeField(
|
||||
read_only=True,
|
||||
)
|
||||
termination = serializers.SerializerMethodField(
|
||||
read_only=True,
|
||||
)
|
||||
termination = GFKSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CableTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
||||
'termination', 'created', 'last_updated',
|
||||
'termination', 'connector', 'positions', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = fields
|
||||
brief_fields = ('id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
brief_fields = (
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'connector', 'positions', 'termination_type',
|
||||
'termination_id',
|
||||
)
|
||||
|
||||
|
||||
class CablePathSerializer(serializers.ModelSerializer):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user