mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 15:47:46 -06:00
Compare commits
778 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f91eeea4 | ||
|
|
ae0ae5fd4e | ||
|
|
5b7486cff8 | ||
|
|
14240318f1 | ||
|
|
18eb9ffae6 | ||
|
|
c0a62793c4 | ||
|
|
dd848d754f | ||
|
|
f058850598 | ||
|
|
0c7220016b | ||
|
|
8c19124717 | ||
|
|
46f4359e1f | ||
|
|
f80452c7d9 | ||
|
|
611f1b57dd | ||
|
|
d1b1a45725 | ||
|
|
6e38f7e532 | ||
|
|
2c1e681984 | ||
|
|
f11ad99983 | ||
|
|
e1ef911d40 | ||
|
|
a4ca585ef2 | ||
|
|
076461a1b6 | ||
|
|
0c7407ebb6 | ||
|
|
f13a3fa549 | ||
|
|
c0a65eb593 | ||
|
|
450a7730d3 | ||
|
|
41ee4b642f | ||
|
|
3ee3c52e14 | ||
|
|
e76a5bfd85 | ||
|
|
59c89a3b9d | ||
|
|
8fc605037a | ||
|
|
311ddf82c5 | ||
|
|
9d65486c64 | ||
|
|
ccce7751a0 | ||
|
|
7252f0b490 | ||
|
|
094d2e586a | ||
|
|
6c1507c88c | ||
|
|
60f48326e1 | ||
|
|
5b985a924b | ||
|
|
ee74989f74 | ||
|
|
e2fc7e8cd7 | ||
|
|
aff55881df | ||
|
|
4d066a075d | ||
|
|
201077b6f6 | ||
|
|
795134c084 | ||
|
|
4f689223b4 | ||
|
|
70ce7293ac | ||
|
|
94a0a3b568 | ||
|
|
69305f0509 | ||
|
|
24f48b11e6 | ||
|
|
ff3b48fa59 | ||
|
|
db3f478598 | ||
|
|
e20ac803f3 | ||
|
|
ea283365e7 | ||
|
|
8211830bd8 | ||
|
|
2a8e0f9404 | ||
|
|
c15cfc26f1 | ||
|
|
4f4e6938eb | ||
|
|
8545a547b9 | ||
|
|
3bb7184f28 | ||
|
|
dd71942a5e | ||
|
|
19fdd5e151 | ||
|
|
f537dc632e | ||
|
|
2221006970 | ||
|
|
5d29c5958b | ||
|
|
64dd46c7e4 | ||
|
|
8df382d976 | ||
|
|
31c58409e1 | ||
|
|
69eb6b11d0 | ||
|
|
1f2d4fd2b3 | ||
|
|
21468fff25 | ||
|
|
4711b4d529 | ||
|
|
29d4859e02 | ||
|
|
4b81d86311 | ||
|
|
38963e7960 | ||
|
|
1584d51433 | ||
|
|
98571c62a6 | ||
|
|
69f525bfd3 | ||
|
|
2b31154834 | ||
|
|
b0948ea018 | ||
|
|
a50e4e3380 | ||
|
|
5564664b13 | ||
|
|
1ae5a2c808 | ||
|
|
0181a25d70 | ||
|
|
60ba4a9830 | ||
|
|
3802a78c9d | ||
|
|
0ca6d73614 | ||
|
|
aa77f8f0d2 | ||
|
|
381796e708 | ||
|
|
62fc7717c8 | ||
|
|
e19451bb4f | ||
|
|
85f588e8c9 | ||
|
|
ea644868a6 | ||
|
|
d08accaaf1 | ||
|
|
f49272cacb | ||
|
|
be8fef0228 | ||
|
|
b584f09223 | ||
|
|
d2968c95df | ||
|
|
7421e5f7d7 | ||
|
|
0b2a43cfcc | ||
|
|
50309d3ab3 | ||
|
|
dd0b16bff5 | ||
|
|
d5443adc74 | ||
|
|
9152ba72f1 | ||
|
|
076ca46ab4 | ||
|
|
02519b270e | ||
|
|
5aa7dedccb | ||
|
|
6383dfa854 | ||
|
|
5a4fb0323b | ||
|
|
e84a282aa6 | ||
|
|
f732493473 | ||
|
|
f66a265fcf | ||
|
|
f1472d218e | ||
|
|
d65c05aacd | ||
|
|
2b28ffa2f4 | ||
|
|
10ec31df3e | ||
|
|
184b1055dc | ||
|
|
eaec25e6c2 | ||
|
|
b63e29610e | ||
|
|
b0db5a8b0a | ||
|
|
d3e2241ff7 | ||
|
|
e90b9f6c19 | ||
|
|
4c1199e009 | ||
|
|
65471068b6 | ||
|
|
c6467a824b | ||
|
|
b1d1f3c6b2 | ||
|
|
574c2e2770 | ||
|
|
aec2d233c9 | ||
|
|
39418f2bbe | ||
|
|
ccda73494f | ||
|
|
443b4ccc57 | ||
|
|
511aedd5db | ||
|
|
2524290099 | ||
|
|
01e8017265 | ||
|
|
8338fc405f | ||
|
|
0a22b3990f | ||
|
|
662cafe416 | ||
|
|
ea961ba8f2 | ||
|
|
8c8774cd2f | ||
|
|
2fe02ddb1f | ||
|
|
e11e8a5d64 | ||
|
|
79bebf7c9b | ||
|
|
8d3b660ce0 | ||
|
|
9de53fe070 | ||
|
|
ecb9fc65b7 | ||
|
|
7b25d0379f | ||
|
|
05d4176d34 | ||
|
|
7b0dff88ae | ||
|
|
1c7604e0fe | ||
|
|
e18dc43aae | ||
|
|
caaad684a4 | ||
|
|
cdd51aee75 | ||
|
|
51851f6c99 | ||
|
|
ab98aa489c | ||
|
|
5829985ca8 | ||
|
|
2fa8e27f05 | ||
|
|
68f92dfd5d | ||
|
|
67aeb380e7 | ||
|
|
f7d91b7139 | ||
|
|
b6e157f393 | ||
|
|
2319fce092 | ||
|
|
a5f1707662 | ||
|
|
6cda55da06 | ||
|
|
c3f2fee633 | ||
|
|
1f575a2a47 | ||
|
|
13c4d13157 | ||
|
|
43fadab3bb | ||
|
|
82a0240d2e | ||
|
|
f2aa35d3d2 | ||
|
|
9c9fcaf42f | ||
|
|
146a51ceba | ||
|
|
b0350e9e96 | ||
|
|
35e346c4b9 | ||
|
|
1987647cc3 | ||
|
|
542534aeba | ||
|
|
908a2824ba | ||
|
|
cab9733b60 | ||
|
|
99e0dcec76 | ||
|
|
9dafb36c88 | ||
|
|
3d7d19b608 | ||
|
|
d650d10cb2 | ||
|
|
7fe45018e9 | ||
|
|
4c4cab87fb | ||
|
|
94c7f64baf | ||
|
|
f369b5f588 | ||
|
|
37065b7c50 | ||
|
|
0a7372460f | ||
|
|
063abc8ef7 | ||
|
|
fb4511d099 | ||
|
|
275560698f | ||
|
|
d4b6fe14c3 | ||
|
|
f1350a1022 | ||
|
|
344fb638fd | ||
|
|
373cc74a33 | ||
|
|
8e95ac42c2 | ||
|
|
ceb941df81 | ||
|
|
d275538116 | ||
|
|
fa38cdbc0d | ||
|
|
7569544b7b | ||
|
|
853a52f3ca | ||
|
|
39a0b15df4 | ||
|
|
a0db10838b | ||
|
|
f2f10dff92 | ||
|
|
7ba45b2887 | ||
|
|
c91eb8f406 | ||
|
|
57a78b3cad | ||
|
|
b755c7dab3 | ||
|
|
9ffd791ae4 | ||
|
|
8af12b22bb | ||
|
|
17ba0a97d5 | ||
|
|
4ae2b4e0b9 | ||
|
|
872691a138 | ||
|
|
3a54ecb522 | ||
|
|
42b590af77 | ||
|
|
b15ecf7649 | ||
|
|
df4f80e773 | ||
|
|
b8b485af4d | ||
|
|
892d6b55ec | ||
|
|
4a3bc8d365 | ||
|
|
e12da72615 | ||
|
|
f95e510060 | ||
|
|
82932ae7a5 | ||
|
|
14fc37a8b8 | ||
|
|
7b23856cc8 | ||
|
|
85f9690377 | ||
|
|
4723500c5f | ||
|
|
2db82a73a5 | ||
|
|
b00eeb86ea | ||
|
|
628e186846 | ||
|
|
cf4a55bc2f | ||
|
|
cab07c7c4b | ||
|
|
7735a539e9 | ||
|
|
68eb6fc3c1 | ||
|
|
fd785fc9a5 | ||
|
|
8d06908353 | ||
|
|
806706ca1d | ||
|
|
044e203eab | ||
|
|
fcc7207b67 | ||
|
|
8dbd3f332b | ||
|
|
f43ec7c05d | ||
|
|
ff9dde54e3 | ||
|
|
3699f16848 | ||
|
|
fee2ac2ebd | ||
|
|
57d3bfcfc9 | ||
|
|
b92e34556f | ||
|
|
b6ff55309e | ||
|
|
305d88ebda | ||
|
|
cdc73d4f56 | ||
|
|
0e50c964d5 | ||
|
|
863fb9aa47 | ||
|
|
298fb00a3e | ||
|
|
d1e8c06d36 | ||
|
|
8ed79d5973 | ||
|
|
85b10b59e4 | ||
|
|
9a53c22833 | ||
|
|
c981b5cba0 | ||
|
|
4ffa823ab8 | ||
|
|
001c7e4b18 | ||
|
|
402136dc8f | ||
|
|
59ee30f056 | ||
|
|
c795068a78 | ||
|
|
5ce080779b | ||
|
|
8d3b296eed | ||
|
|
cfdb985d00 | ||
|
|
af6f0db284 | ||
|
|
491eac184e | ||
|
|
414d33eb26 | ||
|
|
6dd6094088 | ||
|
|
2ec64a2ea2 | ||
|
|
5c34a75032 | ||
|
|
91f33d3289 | ||
|
|
c50dc1eb35 | ||
|
|
dc1331e736 | ||
|
|
afc866eee4 | ||
|
|
b6d93b7c5b | ||
|
|
5d6158dd64 | ||
|
|
e9549ab0bd | ||
|
|
779249ff81 | ||
|
|
66d206a710 | ||
|
|
bfc1cab6df | ||
|
|
5b0c79629e | ||
|
|
7922d3909a | ||
|
|
ee6e2e0af1 | ||
|
|
326a6be91c | ||
|
|
58095e1916 | ||
|
|
3dae077b4d | ||
|
|
7c14c0812b | ||
|
|
3a05eda63a | ||
|
|
d850b3ac7e | ||
|
|
08de6c32c9 | ||
|
|
91fe158c26 | ||
|
|
661b3c4bfb | ||
|
|
35eabc0353 | ||
|
|
ef5bbdb1e2 | ||
|
|
88fae2171d | ||
|
|
de698154cd | ||
|
|
1df05715c2 | ||
|
|
e5524da40e | ||
|
|
50d393e0f9 | ||
|
|
cd08836f3e | ||
|
|
45ac1cfd54 | ||
|
|
dda11ec69e | ||
|
|
7be6206d9d | ||
|
|
4d896573b1 | ||
|
|
988383648c | ||
|
|
d59847537d | ||
|
|
36859d89c8 | ||
|
|
cc50e22928 | ||
|
|
13414dcd25 | ||
|
|
ba8b593351 | ||
|
|
aebfccfd4b | ||
|
|
5a59f2352c | ||
|
|
5164b78da1 | ||
|
|
5561b46a59 | ||
|
|
26b2431cbf | ||
|
|
029605f926 | ||
|
|
0cd173f9df | ||
|
|
414810bdf5 | ||
|
|
f94c1e91ea | ||
|
|
b7129e1456 | ||
|
|
dc6decd404 | ||
|
|
40c6b172f7 | ||
|
|
7cb9cedfe1 | ||
|
|
b43980d660 | ||
|
|
09b612546b | ||
|
|
a99d14c13f | ||
|
|
68f322a03b | ||
|
|
97f0414ff3 | ||
|
|
d5f308d9c9 | ||
|
|
1377eda0ba | ||
|
|
70259b0d04 | ||
|
|
f1466d6da3 | ||
|
|
ca07a88674 | ||
|
|
83010e278c | ||
|
|
dcfd332cbf | ||
|
|
dc3040550d | ||
|
|
3b25db919a | ||
|
|
09f038f997 | ||
|
|
bbdd3804c7 | ||
|
|
a0b9ac7bcc | ||
|
|
8bb0cba949 | ||
|
|
870aa3a265 | ||
|
|
86ada33577 | ||
|
|
869808b3f9 | ||
|
|
57ccbf44b8 | ||
|
|
416caa8f50 | ||
|
|
1e42fecf66 | ||
|
|
c9b00891ed | ||
|
|
497eacbea3 | ||
|
|
f90c591c78 | ||
|
|
175498940e | ||
|
|
eded00cbb3 | ||
|
|
038d7e0fa6 | ||
|
|
b7c9ca720a | ||
|
|
7072f207c0 | ||
|
|
5f59f458f4 | ||
|
|
b6fe613329 | ||
|
|
cd128e557c | ||
|
|
30a5c70260 | ||
|
|
beca978af5 | ||
|
|
98a830a6a0 | ||
|
|
ed2231e34b | ||
|
|
55049bb303 | ||
|
|
c210c6937b | ||
|
|
d2767f39f0 | ||
|
|
1c9d39d3e6 | ||
|
|
f16c6d81cf | ||
|
|
e8d6281007 | ||
|
|
8299845615 | ||
|
|
9ae5865c2d | ||
|
|
c2d0cfdfc0 | ||
|
|
5dd252731e | ||
|
|
7b9436d2b9 | ||
|
|
6a369ac985 | ||
|
|
23d90823a3 | ||
|
|
4bfb6b476c | ||
|
|
0d60099588 | ||
|
|
9a45547cda | ||
|
|
a000ded350 | ||
|
|
424ac29131 | ||
|
|
b7b5a5788f | ||
|
|
9de179cba8 | ||
|
|
94069e76c9 | ||
|
|
df9d67b873 | ||
|
|
6f7fbf7686 | ||
|
|
f32e694499 | ||
|
|
e5900a3fe3 | ||
|
|
6e151b044d | ||
|
|
516bea6a0a | ||
|
|
496cabcc53 | ||
|
|
d051db5083 | ||
|
|
660fc23e15 | ||
|
|
a5a480133f | ||
|
|
68b544c676 | ||
|
|
a8c958ece2 | ||
|
|
f77f7ca0ec | ||
|
|
6b21c8453f | ||
|
|
fa8a8abc98 | ||
|
|
80048bfa2b | ||
|
|
641a9bc6c5 | ||
|
|
0edf9b17f6 | ||
|
|
98cc36c458 | ||
|
|
f3beabba69 | ||
|
|
467fa5a847 | ||
|
|
50f283cf28 | ||
|
|
f49d7008a0 | ||
|
|
1fed564c47 | ||
|
|
bb99c3e6f9 | ||
|
|
8820cac792 | ||
|
|
ada911c20b | ||
|
|
17e01644f5 | ||
|
|
9458521f3e | ||
|
|
8aa73c5900 | ||
|
|
500f213c6b | ||
|
|
cede27b5fe | ||
|
|
c0ca1eaf90 | ||
|
|
b29a5511df | ||
|
|
49e77841e0 | ||
|
|
daf6c8e327 | ||
|
|
9f8068e8d1 | ||
|
|
0b705553a5 | ||
|
|
a799094227 | ||
|
|
d529c1b5b3 | ||
|
|
834f68e6e4 | ||
|
|
83b2102705 | ||
|
|
2f064cdfd1 | ||
|
|
6c28182dd3 | ||
|
|
3cb8c5db28 | ||
|
|
251abdb4dd | ||
|
|
726e4df54b | ||
|
|
bd32a6ac8e | ||
|
|
27d7400c36 | ||
|
|
53e52aeaa8 | ||
|
|
ae6ed97a80 | ||
|
|
34f24de3e4 | ||
|
|
f93d6813a9 | ||
|
|
3ad773beb3 | ||
|
|
be91235858 | ||
|
|
95fc0bbc94 | ||
|
|
9dad7e4daf | ||
|
|
d08ed9fe5f | ||
|
|
82210cc116 | ||
|
|
94d3e76517 | ||
|
|
3f72492a59 | ||
|
|
c0653da736 | ||
|
|
f3d8f1b1fb | ||
|
|
d2391b9c63 | ||
|
|
f8e44c09eb | ||
|
|
2a00519b93 | ||
|
|
3292a2aecc | ||
|
|
b7aa44837f | ||
|
|
17fd6e692e | ||
|
|
2ce8ef5704 | ||
|
|
7b7afd3e7b | ||
|
|
9c2514fce4 | ||
|
|
e04402ed57 | ||
|
|
3eda8d8482 | ||
|
|
79f2f03fb2 | ||
|
|
f7d0db9cd2 | ||
|
|
fab1d3651b | ||
|
|
e5d7578663 | ||
|
|
cf80c1a506 | ||
|
|
4649ab2d85 | ||
|
|
773fd47ca6 | ||
|
|
2fd526b359 | ||
|
|
830cf4b31f | ||
|
|
2826f27153 | ||
|
|
83ac869693 | ||
|
|
bc92f9221a | ||
|
|
c2f85a2877 | ||
|
|
0bbd186635 | ||
|
|
806dcd74ec | ||
|
|
93772e7265 | ||
|
|
9094f07290 | ||
|
|
0aae155c80 | ||
|
|
54233aba1b | ||
|
|
bd3d2c60d9 | ||
|
|
2e75a111ed | ||
|
|
e4b0359b8e | ||
|
|
803e0bfe72 | ||
|
|
a5024a65a0 | ||
|
|
734a00237a | ||
|
|
06f1d15283 | ||
|
|
ff3edc9889 | ||
|
|
8f1acb700d | ||
|
|
519884d167 | ||
|
|
3f0a98acbd | ||
|
|
c023e5f518 | ||
|
|
67c73768c1 | ||
|
|
bbb98083eb | ||
|
|
8bb9f4b8a2 | ||
|
|
96cde7d4af | ||
|
|
ea39c8a4c1 | ||
|
|
7a55832a22 | ||
|
|
7a97d5d4eb | ||
|
|
8305f6d1f5 | ||
|
|
dcececf9c0 | ||
|
|
839afe5ac0 | ||
|
|
c72f25c693 | ||
|
|
5fc373f5cc | ||
|
|
cf9eaf2eff | ||
|
|
04d145d6d8 | ||
|
|
76d73abd81 | ||
|
|
0ec0185d84 | ||
|
|
7b1335825b | ||
|
|
2c2e37e9f0 | ||
|
|
25957bfae3 | ||
|
|
11e2200acf | ||
|
|
f5356b84f6 | ||
|
|
db2d71ed9e | ||
|
|
1bf100ba15 | ||
|
|
7614f423e5 | ||
|
|
318c8b85e9 | ||
|
|
7085fe77da | ||
|
|
2e20d7f02b | ||
|
|
831065b5a1 | ||
|
|
b97167e841 | ||
|
|
19bacc9e23 | ||
|
|
61b61b1bc0 | ||
|
|
7c3318df92 | ||
|
|
d0b85586b9 | ||
|
|
cef0d168a5 | ||
|
|
3a192223a3 | ||
|
|
288a1d23e5 | ||
|
|
7c05db8e2f | ||
|
|
18080a969e | ||
|
|
412430e1c3 | ||
|
|
131e433880 | ||
|
|
b7c0e8b71f | ||
|
|
264652f2c3 | ||
|
|
2cb53a0f7e | ||
|
|
7625a2dd3c | ||
|
|
93da5a39be | ||
|
|
aeb4996ac2 | ||
|
|
330c498fe4 | ||
|
|
5d0a7cb307 | ||
|
|
8c27ff3859 | ||
|
|
736d6cb675 | ||
|
|
200aca470b | ||
|
|
b2dc6c5d3d | ||
|
|
a5ec0ee277 | ||
|
|
f48d7aedce | ||
|
|
bb4f3e1789 | ||
|
|
e0230ed104 | ||
|
|
704fdf9ccd | ||
|
|
d528614cbf | ||
|
|
b5e8157700 | ||
|
|
339776c139 | ||
|
|
87e07e731d | ||
|
|
3991115ae5 | ||
|
|
a30e7bf34f | ||
|
|
43b983054a | ||
|
|
d3364ef4d1 | ||
|
|
24d6941cc4 | ||
|
|
4099dd3a05 | ||
|
|
f420435b82 | ||
|
|
696fe7bc0d | ||
|
|
7c147db324 | ||
|
|
32205045ba | ||
|
|
1ce9192369 | ||
|
|
0a62f75a40 | ||
|
|
2e0f15b35f | ||
|
|
7c60089692 | ||
|
|
0a8788eb97 | ||
|
|
9565addcd4 | ||
|
|
6377d475fc | ||
|
|
fff124ebb1 | ||
|
|
033db83068 | ||
|
|
de5c9ef4b2 | ||
|
|
3c261b05d9 | ||
|
|
ada81e31c9 | ||
|
|
0f68ecda78 | ||
|
|
1902e112f6 | ||
|
|
96565c31d9 | ||
|
|
aa9e68e121 | ||
|
|
15e011ae52 | ||
|
|
a173083e5b | ||
|
|
33c0c8cf6a | ||
|
|
9b5f45aee1 | ||
|
|
93de6c9f88 | ||
|
|
0ad440fea5 | ||
|
|
8235b339ee | ||
|
|
3185cd0b1f | ||
|
|
3a85edba3d | ||
|
|
a573a35349 | ||
|
|
d67d3f8d6d | ||
|
|
1e317f82f5 | ||
|
|
acdebea7f1 | ||
|
|
a090955918 | ||
|
|
70f71e0f57 | ||
|
|
dfdeac4968 | ||
|
|
e84f2e3ad2 | ||
|
|
98ca4f5b5a | ||
|
|
87779b7b88 | ||
|
|
b56cae24c5 | ||
|
|
d48a68317d | ||
|
|
77bd26d17f | ||
|
|
4cdc2601f5 | ||
|
|
ff5c274048 | ||
|
|
561e06e7f0 | ||
|
|
626a446c3d | ||
|
|
26d2da7b98 | ||
|
|
66ed39b4b7 | ||
|
|
fbf91dda7d | ||
|
|
41ff1d0fc9 | ||
|
|
64d8512fc3 | ||
|
|
94804fecd8 | ||
|
|
559dc2f865 | ||
|
|
7c0f32e8ee | ||
|
|
82243732a1 | ||
|
|
61d2158f76 | ||
|
|
68081fb9a2 | ||
|
|
8276933dbb | ||
|
|
0d84338e28 | ||
|
|
2423e0872f | ||
|
|
35c967e6f7 | ||
|
|
b92de63245 | ||
|
|
8b529abfe1 | ||
|
|
a01068949c | ||
|
|
b07e88869a | ||
|
|
94bd27bcf5 | ||
|
|
78ecc8673c | ||
|
|
7e26d92190 | ||
|
|
dbe2f8a6f1 | ||
|
|
e96f5447f4 | ||
|
|
5193fa6483 | ||
|
|
e1e2c76ae1 | ||
|
|
a3e7cab935 | ||
|
|
c06b3374ce | ||
|
|
6f66138a18 | ||
|
|
334c97035e | ||
|
|
1c6a84659c | ||
|
|
3a3ed8bf64 | ||
|
|
001ab1d067 | ||
|
|
4932e4f8c6 | ||
|
|
6f05f17c62 | ||
|
|
cfb3897047 | ||
|
|
8c058dcd45 | ||
|
|
7b70129974 | ||
|
|
6a4becfb46 | ||
|
|
4a7159389e | ||
|
|
a66501250e | ||
|
|
efb41b7433 | ||
|
|
090df05193 | ||
|
|
2c161c01c1 | ||
|
|
fc5a23cc88 | ||
|
|
73f2f9fc63 | ||
|
|
eb4b4a6c8d | ||
|
|
39430e01de | ||
|
|
96015aa590 | ||
|
|
c1720505f3 | ||
|
|
5c338a90a1 | ||
|
|
f04dc55030 | ||
|
|
38bc5de3e8 | ||
|
|
7c56b21095 | ||
|
|
8d0ed99bcd | ||
|
|
8375995680 | ||
|
|
0afd3e6189 | ||
|
|
79cee12b1e | ||
|
|
ba7361bdc7 | ||
|
|
554b44b9f2 | ||
|
|
b44a5ea609 | ||
|
|
487d67768b | ||
|
|
f485a47b48 | ||
|
|
faf1e6a43d | ||
|
|
f193f0d3f9 | ||
|
|
2e78568d4d | ||
|
|
aa5c42683a | ||
|
|
0c72c20d2a | ||
|
|
9c6938e7ae | ||
|
|
811c21ec7e | ||
|
|
84c14aadc7 | ||
|
|
f1f0d9cd0d | ||
|
|
717fd760df | ||
|
|
075f4907ef | ||
|
|
b7317bfe29 | ||
|
|
01d3c062f2 | ||
|
|
6af5a884cd | ||
|
|
6015c47587 | ||
|
|
33ea8763d5 | ||
|
|
2c2c2e9060 | ||
|
|
64dad7dbd2 | ||
|
|
176bd2396b | ||
|
|
e16942dea5 | ||
|
|
12efcec3b0 | ||
|
|
a7b6c40596 | ||
|
|
b95773938d | ||
|
|
6898ae7106 | ||
|
|
909b83c537 | ||
|
|
fb9da87abb | ||
|
|
bdf359470e | ||
|
|
4c475c1b33 | ||
|
|
438b4b4758 | ||
|
|
01f791a44e | ||
|
|
43f2d4a331 | ||
|
|
95ed07a95e | ||
|
|
ec0560a2c5 | ||
|
|
ac2cd552b9 | ||
|
|
1c73bd5079 | ||
|
|
138af27bf7 | ||
|
|
445e16f668 | ||
|
|
90e9f34494 | ||
|
|
5271680483 | ||
|
|
38f6d22d2d | ||
|
|
8b80b0c3df | ||
|
|
8e1535f7ec | ||
|
|
3e7922e41e | ||
|
|
1a4f8c5422 | ||
|
|
66c4d23119 | ||
|
|
d66fc8f661 | ||
|
|
031876964f | ||
|
|
5a6190e321 | ||
|
|
18c3bb673f | ||
|
|
6463fd902c | ||
|
|
c63766c4c6 | ||
|
|
af6237e12e | ||
|
|
00328226ec | ||
|
|
b31ba4e9d2 | ||
|
|
4be5d3f9e9 | ||
|
|
53154746fc | ||
|
|
2f4c1b6e8f | ||
|
|
045ec7d3a0 | ||
|
|
b73db750e5 | ||
|
|
3f766ffea8 | ||
|
|
f28761202f | ||
|
|
6d1f07df05 | ||
|
|
eb9f2b36ab | ||
|
|
ca59cd1eb8 | ||
|
|
2bd29127dc | ||
|
|
3eef6363fd | ||
|
|
d451f30bfc | ||
|
|
105956f8e6 | ||
|
|
39256afb67 | ||
|
|
69aaf28b9c | ||
|
|
b806220074 | ||
|
|
d2bdf4e822 | ||
|
|
3ab5682e7a | ||
|
|
c0010ec100 | ||
|
|
6897c5fadd | ||
|
|
745aa23ed6 | ||
|
|
9089f5cf67 | ||
|
|
dd79aae137 | ||
|
|
26e470f521 | ||
|
|
a34c8b80e5 | ||
|
|
854a12982f | ||
|
|
cf173d4f50 | ||
|
|
7041486b93 | ||
|
|
548a8c3be3 | ||
|
|
087a018faf | ||
|
|
e09024e86f | ||
|
|
1757102536 | ||
|
|
9c8432cf13 | ||
|
|
c262af550d | ||
|
|
d9c6609b24 | ||
|
|
339bcb89bb | ||
|
|
b5884a5b54 | ||
|
|
c818d63043 | ||
|
|
c9c537a1b9 | ||
|
|
1be748b479 | ||
|
|
376c776520 | ||
|
|
a1f271d7d9 | ||
|
|
724997cb48 | ||
|
|
f3fe3f9a18 | ||
|
|
357a5d1e65 | ||
|
|
460e3fd5d6 | ||
|
|
257c0afdb5 | ||
|
|
ed3bc7cdcc | ||
|
|
bd181ac84f | ||
|
|
d1f5988db7 | ||
|
|
a5b99e7148 | ||
|
|
114500e7f4 | ||
|
|
d9f178e315 | ||
|
|
7337630704 | ||
|
|
0fdd081869 | ||
|
|
a9761e8dd2 | ||
|
|
1f1a05dc67 | ||
|
|
14b065cf5f | ||
|
|
47c3a20fda | ||
|
|
86aed4e073 | ||
|
|
19c984bdab |
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -13,11 +13,8 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: >
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v3.0.4
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.1.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.4
|
||||
placeholder: v3.1.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -38,6 +38,19 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Yarn Package Manager
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Setup Node.js with Yarn Caching
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: yarn
|
||||
cache-dependency-path: netbox/project-static/yarn.lock
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
run: yarn --cwd netbox/project-static
|
||||
|
||||
- name: Install dependencies & set up configuration
|
||||
run: |
|
||||
@@ -45,7 +58,6 @@ jobs:
|
||||
pip install -r requirements.txt
|
||||
pip install pycodestyle coverage
|
||||
ln -s configuration.testing.py netbox/netbox/configuration.py
|
||||
yarn --cwd netbox/project-static
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
@@ -63,7 +75,7 @@ jobs:
|
||||
run: scripts/verify-bundles.sh
|
||||
|
||||
- name: Run tests
|
||||
run: coverage run --source="netbox/" netbox/manage.py test netbox/
|
||||
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
|
||||
|
||||
- name: Show coverage report
|
||||
run: coverage report --skip-covered --omit *migrations*
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ yarn-error.log*
|
||||
!/netbox/project-static/docs/.info
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/local/*
|
||||
/netbox/reports/*
|
||||
!/netbox/reports/__init__.py
|
||||
/netbox/scripts/*
|
||||
|
||||
@@ -16,13 +16,6 @@ categories for discussions:
|
||||
feature request
|
||||
* **Q&A** - Request help with installing or using NetBox
|
||||
|
||||
### Mailing List
|
||||
|
||||
We also have a Google Groups [mailing list](https://groups.google.com/g/netbox-discuss)
|
||||
for general discussion, however we're encouraging people to use GitHub
|
||||
discussions where possible, as it's much easier for newcomers to review past
|
||||
discussions.
|
||||
|
||||
### Slack
|
||||
|
||||
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://netdev.chat/).
|
||||
@@ -76,14 +69,10 @@ free to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||
|
||||
* Due to a large backlog of feature requests, we are not currently accepting
|
||||
any proposals which substantially extend NetBox's functionality beyond its
|
||||
current feature set. This includes the introduction of any new views or models
|
||||
which have not already been proposed in an existing feature request.
|
||||
|
||||
* Before filing a new feature request, consider raising your idea on the
|
||||
mailing list first. Feedback you receive there will help validate and shape the
|
||||
proposed feature before filing a formal issue.
|
||||
* Before filing a new feature request, consider raising your idea in a
|
||||
[GitHub discussion](https://github.com/netbox-community/netbox/discussions)
|
||||
first. Feedback you receive there will help validate and shape the proposed
|
||||
feature before filing a formal issue.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||
describe the functionality and data model(s) being proposed. The more effort
|
||||
|
||||
38
README.md
38
README.md
@@ -5,11 +5,46 @@
|
||||

|
||||
|
||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
||||
network automation. Initially conceived by the network engineering team at
|
||||
network automation, used by thousands of organizations around the world.
|
||||
Initially conceived by the network engineering team at
|
||||
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
|
||||
to address the needs of network and infrastructure engineers. It is intended to
|
||||
function as a domain-specific source of truth for network operations.
|
||||
|
||||
Myriad infrastructure components can be modeled in NetBox, including:
|
||||
|
||||
* Hierarchical regions, site groups, sites, and locations
|
||||
* Racks, devices, and device components
|
||||
* Cables and wireless connections
|
||||
* Power distribution
|
||||
* Data circuits and providers
|
||||
* Virtual machines and clusters
|
||||
* IP prefixes, ranges, and addresses
|
||||
* VRFs and route targets
|
||||
* FHRP groups (VRRP, HSRP, etc.)
|
||||
* AS numbers
|
||||
* VLANs and scoped VLAN groups
|
||||
* Organizational tenants and contacts
|
||||
|
||||
In addition to its extensive built-in models and functionality, NetBox can be
|
||||
customized and extended through the use of:
|
||||
|
||||
* Custom fields
|
||||
* Custom links
|
||||
* Configuration contexts
|
||||
* Custom model validation rules
|
||||
* Reports
|
||||
* Custom scripts
|
||||
* Export templates
|
||||
* Conditional webhooks
|
||||
* Plugins
|
||||
* Single sign-on (SSO) authentication
|
||||
* NAPALM integration
|
||||
* Detailed change logging
|
||||
|
||||
NetBox also features a complete REST API as well as a GraphQL API for easily
|
||||
integrating with other tools and systems.
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
||||
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
|
||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
|
||||
@@ -33,7 +68,6 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
|
||||
|
||||
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
|
||||
* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
|
||||
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://github.com/django/django
|
||||
Django
|
||||
Django<4.0
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
@@ -98,9 +98,13 @@ psycopg2-binary
|
||||
# https://github.com/yaml/pyyaml
|
||||
PyYAML
|
||||
|
||||
# In-memory key/value store used for caching and queuing
|
||||
# https://github.com/andymccurdy/redis-py
|
||||
redis
|
||||
# Social authentication framework
|
||||
# https://github.com/python-social-auth/social-core
|
||||
social-auth-core
|
||||
|
||||
# Django app for social-auth-core
|
||||
# https://github.com/python-social-auth/social-app-django
|
||||
social-auth-app-django
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite
|
||||
|
||||
0
contrib/netbox-housekeeping.sh
Normal file → Executable file
0
contrib/netbox-housekeeping.sh
Normal file → Executable file
@@ -29,7 +29,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, the [`NAPALM_USERNAME`](../configuration/optional-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/optional-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
By default, the [`NAPALM_USERNAME`](../configuration/dynamic-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/dynamic-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
{!models/extras/webhook.md!}
|
||||
|
||||
## Conditional Webhooks
|
||||
|
||||
A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
|
||||
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
|
||||
|
||||
37
docs/administration/authentication.md
Normal file
37
docs/administration/authentication.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Authentication
|
||||
|
||||
## Local Authentication
|
||||
|
||||
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
|
||||
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](./permissions.md) may also be assigned to users and/or groups within the admin UI.
|
||||
|
||||
## Remote Authentication
|
||||
|
||||
NetBox may be configured to provide user authenticate via a remote backend in addition to local authentication. This is done by setting the `REMOTE_AUTH_BACKEND` configuration parameter to a suitable backend class. NetBox provides several options for remote authentication.
|
||||
|
||||
### LDAP Authentication
|
||||
|
||||
```python
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
|
||||
```
|
||||
|
||||
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../installation/6-ldap.md) for more detail about this backend.
|
||||
|
||||
### HTTP Header Authentication
|
||||
|
||||
```python
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||
```
|
||||
|
||||
Another option for remote authentication in NetBox is to enable HTTP header-based user assignment. The front end HTTP server (e.g. nginx or Apache) performs client authentication as a process external to NetBox, and passes information about the authenticated user via HTTP headers. By default, the user is assigned via the `REMOTE_USER` header, but this can be customized via the `REMOTE_AUTH_HEADER` configuration parameter.
|
||||
|
||||
### Single Sign-On (SSO)
|
||||
|
||||
```python
|
||||
REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
|
||||
```
|
||||
|
||||
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
|
||||
|
||||
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter.
|
||||
@@ -3,8 +3,15 @@
|
||||
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
|
||||
|
||||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be copied into your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside of scheduled execution times will not interfere with its operation.
|
||||
```shell
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
!!! note
|
||||
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Permissions
|
||||
|
||||
NetBox v2.9 introduced a new object-based permissions framework, which replace's Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
|
||||
|
||||
{!models/users/objectpermission.md!}
|
||||
|
||||
|
||||
180
docs/configuration/dynamic-settings.md
Normal file
180
docs/configuration/dynamic-settings.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Dynamic Configuration Settings
|
||||
|
||||
These configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These setting may also be overridden in `configuration.py`; this will prevent them from being modified via the UI.
|
||||
|
||||
---
|
||||
|
||||
## ALLOWED_URL_SCHEMES
|
||||
|
||||
Default: `('file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp')`
|
||||
|
||||
A list of permitted URL schemes referenced when rendering links within NetBox. Note that only the schemes specified in this list will be accepted: If adding your own, be sure to replicate all of the default values as well (excluding those schemes which are not desirable).
|
||||
|
||||
---
|
||||
|
||||
## BANNER_TOP
|
||||
|
||||
## BANNER_BOTTOM
|
||||
|
||||
Setting these variables will display custom content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set:
|
||||
|
||||
```python
|
||||
BANNER_TOP = 'Your banner text'
|
||||
BANNER_BOTTOM = BANNER_TOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BANNER_LOGIN
|
||||
|
||||
This defines custom content to be displayed on the login page above the login form. HTML is allowed.
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
|
||||
changes in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
"dcim.site": [
|
||||
{
|
||||
"name": {
|
||||
"min_length": 5,
|
||||
"max_length": 30
|
||||
}
|
||||
},
|
||||
"my_plugin.validators.Validator1"
|
||||
],
|
||||
"dim.device": [
|
||||
"my_plugin.validators.Validator1"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
Default: False
|
||||
|
||||
By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True.
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## MAINTENANCE_MODE
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
||||
|
||||
---
|
||||
|
||||
## MAPS_URL
|
||||
|
||||
Default: `https://maps.google.com/?q=` (Google Maps)
|
||||
|
||||
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
|
||||
|
||||
---
|
||||
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
Default: 1000
|
||||
|
||||
A web user or API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This parameter defines the maximum acceptable limit. Setting this to `0` or `None` will allow a client to retrieve _all_ matching objects at once with no limit by specifying `?limit=0`.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_USERNAME
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional.
|
||||
|
||||
!!! note
|
||||
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_ARGS
|
||||
|
||||
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
|
||||
|
||||
```python
|
||||
NAPALM_ARGS = {
|
||||
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
|
||||
'port': 2222,
|
||||
}
|
||||
```
|
||||
|
||||
Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
|
||||
|
||||
```python
|
||||
NAPALM_USERNAME = 'username'
|
||||
NAPALM_PASSWORD = 'MySecretPassword'
|
||||
NAPALM_ARGS = {
|
||||
'secret': NAPALM_PASSWORD,
|
||||
# Include any additional args here
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_TIMEOUT
|
||||
|
||||
Default: 30 seconds
|
||||
|
||||
The amount of time (in seconds) to wait for NAPALM to connect to a device.
|
||||
|
||||
---
|
||||
|
||||
## PAGINATE_COUNT
|
||||
|
||||
Default: 50
|
||||
|
||||
The default maximum number of objects to display per page within each list of objects.
|
||||
|
||||
---
|
||||
|
||||
## PREFER_IPV4
|
||||
|
||||
Default: False
|
||||
|
||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
|
||||
Default: 22
|
||||
|
||||
Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`.
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
|
||||
Default: 220
|
||||
|
||||
Default width (in pixels) of a unit within a rack elevation.
|
||||
@@ -1,18 +1,22 @@
|
||||
# NetBox Configuration
|
||||
|
||||
NetBox's local configuration is stored in `$INSTALL_ROOT/netbox/netbox/configuration.py`. An example configuration is provided as `configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file.
|
||||
NetBox's local configuration is stored in `$INSTALL_ROOT/netbox/netbox/configuration.py`. An example configuration is provided as `configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file. While NetBox has many configuration settings, only a few of them must be defined at the time of installation: these are defined under "required settings" below.
|
||||
|
||||
While NetBox has many configuration settings, only a few of them must be defined at the time of installation.
|
||||
Some configuration parameters may alternatively be defined either in `configuration.py` or within the administrative section of the user interface. Settings which are "hard-coded" in the configuration file take precedence over those defined via the UI.
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
* [Required settings](required-settings.md)
|
||||
* [Optional settings](optional-settings.md)
|
||||
* [Dynamic settings](dynamic-settings.md)
|
||||
* [Remote authentication settings](remote-authentication.md)
|
||||
|
||||
## Changing the Configuration
|
||||
|
||||
Configuration settings may be changed at any time. However, the WSGI service (e.g. Gunicorn) must be restarted before the changes will take effect:
|
||||
The configuration file may be modified at any time. However, the WSGI service (e.g. Gunicorn) must be restarted before the changes will take effect:
|
||||
|
||||
```no-highlight
|
||||
$ sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
Configuration parameters which are set via the admin UI (those listed under "dynamic settings") take effect immediately.
|
||||
|
||||
@@ -13,33 +13,6 @@ ADMINS = [
|
||||
|
||||
---
|
||||
|
||||
## ALLOWED_URL_SCHEMES
|
||||
|
||||
Default: `('file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp')`
|
||||
|
||||
A list of permitted URL schemes referenced when rendering links within NetBox. Note that only the schemes specified in this list will be accepted: If adding your own, be sure to replicate all of the default values as well (excluding those schemes which are not desirable).
|
||||
|
||||
---
|
||||
|
||||
## BANNER_TOP
|
||||
|
||||
## BANNER_BOTTOM
|
||||
|
||||
Setting these variables will display custom content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set:
|
||||
|
||||
```python
|
||||
BANNER_TOP = 'Your banner text'
|
||||
BANNER_BOTTOM = BANNER_TOP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BANNER_LOGIN
|
||||
|
||||
This defines custom content to be displayed on the login page above the login form. HTML is allowed.
|
||||
|
||||
---
|
||||
|
||||
## BASE_PATH
|
||||
|
||||
Default: None
|
||||
@@ -52,18 +25,6 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
|
||||
changes in the database indefinitely.
|
||||
|
||||
!!! warning
|
||||
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
||||
Default: False
|
||||
@@ -88,22 +49,6 @@ CORS_ORIGIN_WHITELIST = [
|
||||
|
||||
---
|
||||
|
||||
## CUSTOM_VALIDATORS
|
||||
|
||||
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
|
||||
|
||||
```python
|
||||
CUSTOM_VALIDATORS = {
|
||||
'dcim.site': (
|
||||
Validator1,
|
||||
Validator2,
|
||||
Validator3
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DEBUG
|
||||
|
||||
Default: False
|
||||
@@ -168,14 +113,6 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
Default: False
|
||||
|
||||
By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True.
|
||||
|
||||
---
|
||||
|
||||
## EXEMPT_VIEW_PERMISSIONS
|
||||
|
||||
Default: Empty list
|
||||
@@ -203,14 +140,6 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
||||
|
||||
---
|
||||
|
||||
## GRAPHQL_ENABLED
|
||||
|
||||
Default: True
|
||||
|
||||
Setting this to False will disable the GraphQL API.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
@@ -299,30 +228,6 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
|
||||
|
||||
---
|
||||
|
||||
## MAINTENANCE_MODE
|
||||
|
||||
Default: False
|
||||
|
||||
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
||||
|
||||
---
|
||||
|
||||
## MAPS_URL
|
||||
|
||||
Default: `https://maps.google.com/?q=` (Google Maps)
|
||||
|
||||
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
|
||||
|
||||
---
|
||||
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
Default: 1000
|
||||
|
||||
A web user or API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This parameter defines the maximum acceptable limit. Setting this to `0` or `None` will allow a client to retrieve _all_ matching objects at once with no limit by specifying `?limit=0`.
|
||||
|
||||
---
|
||||
|
||||
## MEDIA_ROOT
|
||||
|
||||
Default: $INSTALL_ROOT/netbox/media/
|
||||
@@ -339,57 +244,6 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_USERNAME
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the supported [NAPALM integration](../additional-features/napalm.md), if installed. Both parameters are optional.
|
||||
|
||||
!!! note
|
||||
If SSH public key authentication has been set up on the remote device(s) for the system account under which NetBox runs, these parameters are not needed.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_ARGS
|
||||
|
||||
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](https://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
|
||||
|
||||
```python
|
||||
NAPALM_ARGS = {
|
||||
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
|
||||
'port': 2222,
|
||||
}
|
||||
```
|
||||
|
||||
Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
|
||||
|
||||
```python
|
||||
NAPALM_USERNAME = 'username'
|
||||
NAPALM_PASSWORD = 'MySecretPassword'
|
||||
NAPALM_ARGS = {
|
||||
'secret': NAPALM_PASSWORD,
|
||||
# Include any additional args here
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_TIMEOUT
|
||||
|
||||
Default: 30 seconds
|
||||
|
||||
The amount of time (in seconds) to wait for NAPALM to connect to a device.
|
||||
|
||||
---
|
||||
|
||||
## PAGINATE_COUNT
|
||||
|
||||
Default: 50
|
||||
|
||||
The default maximum number of objects to display per page within each list of objects.
|
||||
|
||||
---
|
||||
|
||||
## PLUGINS
|
||||
|
||||
Default: Empty
|
||||
@@ -423,137 +277,6 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
|
||||
|
||||
---
|
||||
|
||||
## PREFER_IPV4
|
||||
|
||||
Default: False
|
||||
|
||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
|
||||
Default: 22
|
||||
|
||||
Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`.
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
|
||||
Default: 220
|
||||
|
||||
Default width (in pixels) of a unit within a rack elevation.
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_AUTO_CREATE_USER
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_BACKEND
|
||||
|
||||
Default: `'netbox.authentication.RemoteUserBackend'`
|
||||
|
||||
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
|
||||
|
||||
* `netbox.authentication.RemoteUserBackend`
|
||||
* `netbox.authentication.LDAPBackend`
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_DEFAULT_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_DEFAULT_PERMISSIONS
|
||||
|
||||
Default: `{}` (Empty dictionary)
|
||||
|
||||
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SYNC_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to sync remote user groups by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User` it needs to be set to `HTTP_X_REMOTE_USER`. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_GROUP'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSER_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Superuser on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_USERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SEPARATOR
|
||||
|
||||
Default: `|` (Pipe)
|
||||
|
||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## RELEASE_CHECK_URL
|
||||
|
||||
Default: None (disabled)
|
||||
|
||||
110
docs/configuration/remote-authentication.md
Normal file
110
docs/configuration/remote-authentication.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Remote Authentication Settings
|
||||
|
||||
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be true in order for these settings to take effect.
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_AUTO_CREATE_USER
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_BACKEND
|
||||
|
||||
Default: `'netbox.authentication.RemoteUserBackend'`
|
||||
|
||||
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
|
||||
|
||||
* `netbox.authentication.RemoteUserBackend`
|
||||
* `netbox.authentication.LDAPBackend`
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_DEFAULT_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_DEFAULT_PERMISSIONS
|
||||
|
||||
Default: `{}` (Empty dictionary)
|
||||
|
||||
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SYNC_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
||||
NetBox can be configured to sync remote user groups by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User` it needs to be set to `HTTP_X_REMOTE_USER`. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_HEADER
|
||||
|
||||
Default: `'HTTP_REMOTE_USER_GROUP'`
|
||||
|
||||
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSER_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Superuser on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_SUPERUSERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_GROUPS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_STAFF_USERS
|
||||
|
||||
Default: `[]` (Empty list)
|
||||
|
||||
The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_GROUP_SEPARATOR
|
||||
|
||||
Default: `|` (Pipe)
|
||||
|
||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
@@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
|
||||
|
||||
## DATABASE
|
||||
|
||||
NetBox requires access to a PostgreSQL 9.6 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
NetBox requires access to a PostgreSQL 10 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||
|
||||
* `NAME` - Database name
|
||||
* `USER` - PostgreSQL username
|
||||
|
||||
5
docs/core-functionality/contacts.md
Normal file
5
docs/core-functionality/contacts.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Contacts
|
||||
|
||||
{!models/tenancy/contact.md!}
|
||||
{!models/tenancy/contactgroup.md!}
|
||||
{!models/tenancy/contactrole.md!}
|
||||
@@ -27,3 +27,13 @@ Device components represent discrete objects within a device which are used to t
|
||||
---
|
||||
|
||||
{!models/dcim/cable.md!}
|
||||
|
||||
In the example below, three individual cables comprise a path between devices A and D:
|
||||
|
||||

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -17,3 +17,11 @@
|
||||
|
||||
{!models/ipam/vrf.md!}
|
||||
{!models/ipam/routetarget.md!}
|
||||
|
||||
---
|
||||
|
||||
{!models/ipam/fhrpgroup.md!}
|
||||
|
||||
---
|
||||
|
||||
{!models/ipam/asn.md!}
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
# Example Power Topology
|
||||
|
||||

|
||||

|
||||
|
||||
8
docs/core-functionality/wireless.md
Normal file
8
docs/core-functionality/wireless.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Wireless Networks
|
||||
|
||||
{!models/wireless/wirelesslan.md!}
|
||||
{!models/wireless/wirelesslangroup.md!}
|
||||
|
||||
---
|
||||
|
||||
{!models/wireless/wirelesslink.md!}
|
||||
@@ -1 +0,0 @@
|
||||
{!models/extras/customlink.md!}
|
||||
@@ -45,6 +45,20 @@ Defining script variables is optional: You may create a script with only a `run(
|
||||
|
||||
Any output generated by the script during its execution will be displayed under the "output" tab in the UI.
|
||||
|
||||
By default, scripts within a module are ordered alphabetically in the scripts list page. To return scripts in a specific order, you can define the `script_order` variable at the end of your module. The `script_order` variable is a tuple which contains each Script class in the desired order. Any scripts that are omitted from this list will be listed last.
|
||||
|
||||
```python
|
||||
from extras.scripts import Script
|
||||
|
||||
class MyCustomScript(Script):
|
||||
...
|
||||
|
||||
class AnotherCustomScript(Script):
|
||||
...
|
||||
|
||||
script_order = (MyCustomScript, AnotherCustomScript)
|
||||
```
|
||||
|
||||
## Module Attributes
|
||||
|
||||
### `name`
|
||||
@@ -63,6 +77,10 @@ This is the human-friendly names of your script. If omitted, the class name will
|
||||
|
||||
A human-friendly description of what your script does.
|
||||
|
||||
### `field_order`
|
||||
|
||||
By default, script variables will be ordered in the form as they are defined in the script. `field_order` may be defined as an iterable of field names to determine the order in which variables are rendered. Any fields not included in this iterable be listed last.
|
||||
|
||||
### `commit_default`
|
||||
|
||||
The checkbox to commit database changes when executing a script is checked by default. Set `commit_default` to False under the script's Meta class to leave this option unchecked by default.
|
||||
@@ -226,7 +244,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
||||
!!! note
|
||||
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
||||
|
||||

|
||||

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

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -12,3 +12,5 @@ Some devices house child devices which share physical resources, like space and
|
||||
|
||||
!!! note
|
||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device.
|
||||
|
||||
A device type may optionally specify an airflow direction, such as front-to-rear, rear-to-front, or passive. Airflow direction may also be set separately per device. If it is not defined for a device at the time of its creation, it will inherit the airflow setting of its device type.
|
||||
|
||||
@@ -11,6 +11,17 @@ Interfaces may be physical or virtual in nature, but only physical interfaces ma
|
||||
|
||||
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
|
||||
|
||||
### Wireless Interfaces
|
||||
|
||||
Wireless interfaces may additionally track the following attributes:
|
||||
|
||||
* **Role** - AP or station
|
||||
* **Channel** - One of several standard wireless channels
|
||||
* **Channel Frequency** - The transmit frequency
|
||||
* **Channel Width** - Channel bandwidth
|
||||
|
||||
If a predefined channel is selected, the frequency and width attributes will be assigned automatically. If no channel is selected, these attributes may be defined manually.
|
||||
|
||||
### IP Address Assignment
|
||||
|
||||
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
|
||||
|
||||
The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.)
|
||||
Each location must have a name that is unique within its parent site and location, if any.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Racks
|
||||
|
||||
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles.
|
||||
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique.
|
||||
|
||||
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Regions
|
||||
|
||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||
|
||||
Each region must have a name that is unique within its parent region, if any.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Site Groups
|
||||
|
||||
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.
|
||||
|
||||
Each site group must have a name that is unique within its parent group, if any.
|
||||
|
||||
@@ -11,14 +11,16 @@ Within the database, custom fields are stored as JSON data directly alongside ea
|
||||
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||
|
||||
* Text: Free-form text (up to 255 characters)
|
||||
* Long text: Free-form of any length; supports Markdown rendering
|
||||
* Integer: A whole number (positive or negative)
|
||||
* Boolean: True or false
|
||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||
* URL: This will be presented as a link in the web UI
|
||||
* JSON: Arbitrary data stored in JSON format
|
||||
* Selection: A selection of one of several pre-defined custom choices
|
||||
* Multiple selection: A selection field which supports the assignment of multiple values
|
||||
|
||||
Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||
Each custom field must have a name. This should be a simple database-friendly string (e.g. `tps_report`) and may contain only alphanumeric characters and underscores. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
|
||||
|
||||
Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields, or the exact value of a choice for selection fields.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Custom Links
|
||||
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
|
||||
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
||||
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the Netbox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
|
||||
|
||||
For example, you might define a link like this:
|
||||
|
||||
@@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
|
||||
## Link Groups
|
||||
|
||||
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
|
||||
|
||||
## Table Columns
|
||||
|
||||
Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL.
|
||||
|
||||
@@ -15,6 +15,3 @@ The `tag` filter can be specified multiple times to match only objects which hav
|
||||
```no-highlight
|
||||
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||
```
|
||||
|
||||
!!! note
|
||||
Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label.
|
||||
|
||||
@@ -17,6 +17,7 @@ A webhook is a mechanism for conveying to some external system a change that too
|
||||
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
||||
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
||||
* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
|
||||
* **Conditions** - An optional set of conditions evaluated to determine whether the webhook fires for a given object.
|
||||
* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!)
|
||||
* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional).
|
||||
|
||||
|
||||
15
docs/models/ipam/asn.md
Normal file
15
docs/models/ipam/asn.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# ASN
|
||||
|
||||
ASN is short for Autonomous System Number. This identifier is used in the BGP protocol to identify which "autonomous system" a particular prefix is originating and transiting through.
|
||||
|
||||
The AS number model within NetBox allows you to model some of this real-world relationship.
|
||||
|
||||
Within NetBox:
|
||||
|
||||
* AS numbers are globally unique
|
||||
* Each AS number must be associated with a RIR (ARIN, RFC 6996, etc)
|
||||
* Each AS number can be associated with many different sites
|
||||
* Each site can have many different AS numbers
|
||||
* Each AS number can be assigned to a single tenant
|
||||
|
||||
|
||||
16
docs/models/ipam/fhrpgroup.md
Normal file
16
docs/models/ipam/fhrpgroup.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# FHRP Group
|
||||
|
||||
A first-hop redundancy protocol (FHRP) enables multiple physical interfaces to present a virtual IP address in a redundant manner. Example of such protocols include:
|
||||
|
||||
* Hot Standby Router Protocol (HSRP)
|
||||
* Virtual Router Redundancy Protocol (VRRP)
|
||||
* Common Address Redundancy Protocol (CARP)
|
||||
* Gateway Load Balancing Protocol (GLBP)
|
||||
|
||||
NetBox models these redundancy groups by protocol and group ID. Each group may optionally be assigned an authentication type and key. (Note that the authentication key is stored as a plaintext value in NetBox.) Each group may be assigned or more virtual IPv4 and/or IPv6 addresses.
|
||||
|
||||
## FHRP Group Assignments
|
||||
|
||||
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
|
||||
|
||||
Interfaces are assigned to FHRP groups under the interface detail view.
|
||||
31
docs/models/tenancy/contact.md
Normal file
31
docs/models/tenancy/contact.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contacts
|
||||
|
||||
A contact represent an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. Contacts can be arranged within nested contact groups.
|
||||
|
||||
Each contact must include a name, which is unique to its parent group (if any). The following optional descriptors are also available:
|
||||
|
||||
* Title
|
||||
* Phone
|
||||
* Email
|
||||
* Address
|
||||
|
||||
## Contact Assignment
|
||||
|
||||
Each contact can be assigned to one or more objects, allowing for the efficient reuse of contact information. When assigning a contact to an object, the user may optionally specify a role and/or priority (primary, secondary, tertiary, or inactive) to better convey the nature of the contact's relationship to the assigned object.
|
||||
|
||||
The following models support the assignment of contacts:
|
||||
|
||||
* circuits.Circuit
|
||||
* circuits.Provider
|
||||
* dcim.Device
|
||||
* dcim.Location
|
||||
* dcim.Manufacturer
|
||||
* dcim.PowerPanel
|
||||
* dcim.Rack
|
||||
* dcim.Region
|
||||
* dcim.Site
|
||||
* dcim.SiteGroup
|
||||
* tenancy.Tenant
|
||||
* virtualization.Cluster
|
||||
* virtualization.ClusterGroup
|
||||
* virtualization.VirtualMachine
|
||||
3
docs/models/tenancy/contactgroup.md
Normal file
3
docs/models/tenancy/contactgroup.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contact Groups
|
||||
|
||||
Contacts can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated.
|
||||
3
docs/models/tenancy/contactrole.md
Normal file
3
docs/models/tenancy/contactrole.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contact Roles
|
||||
|
||||
Contacts can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts.
|
||||
@@ -1,5 +1,5 @@
|
||||
# Clusters
|
||||
|
||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant.
|
||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any.
|
||||
|
||||
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||
|
||||
11
docs/models/wireless/wirelesslan.md
Normal file
11
docs/models/wireless/wirelesslan.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Wireless LANs
|
||||
|
||||
A wireless LAN is a set of interfaces connected via a common wireless channel. Each instance must have an SSID, and may optionally be correlated to a VLAN. Wireless LANs can be arranged into hierarchical groups.
|
||||
|
||||
An interface may be attached to multiple wireless LANs, provided they are all operating on the same channel. Only wireless interfaces may be attached to wireless LANs.
|
||||
|
||||
Each wireless LAN may have authentication attributes associated with it, including:
|
||||
|
||||
* Authentication type
|
||||
* Cipher
|
||||
* Pre-shared key
|
||||
3
docs/models/wireless/wirelesslangroup.md
Normal file
3
docs/models/wireless/wirelesslangroup.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Wireless LAN Groups
|
||||
|
||||
Wireless LAN groups can be used to organize and classify wireless LANs. These groups are hierarchical: groups can be nested within parent groups. However, each wireless LAN may assigned only to one group.
|
||||
9
docs/models/wireless/wirelesslink.md
Normal file
9
docs/models/wireless/wirelesslink.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Wireless Links
|
||||
|
||||
A wireless link represents a connection between exactly two wireless interfaces. It may optionally be assigned an SSID and a description. It may also have a status assigned to it, similar to the cable model.
|
||||
|
||||
Each wireless link may have authentication attributes associated with it, including:
|
||||
|
||||
* Authentication type
|
||||
* Cipher
|
||||
* Pre-shared key
|
||||
@@ -1,5 +1,8 @@
|
||||
# Plugin Development
|
||||
|
||||
!!! info "Help Improve the NetBox Plugins Framework!"
|
||||
We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338).
|
||||
|
||||
This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox.
|
||||
|
||||
Plugins can do a lot, including:
|
||||
|
||||
125
docs/reference/conditions.md
Normal file
125
docs/reference/conditions.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Conditions
|
||||
|
||||
Conditions are NetBox's mechanism for evaluating whether a set data meets a prescribed set of conditions. It allows the author to convey simple logic by declaring an arbitrary number of attribute-value-operation tuples nested within a hierarchy of logical AND and OR statements.
|
||||
|
||||
## Conditions
|
||||
|
||||
A condition is expressed as a JSON object with the following keys:
|
||||
|
||||
| Key name | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| attr | Yes | - | Name of the key within the data being evaluated |
|
||||
| value | Yes | - | The reference value to which the given data will be compared |
|
||||
| op | No | `eq` | The logical operation to be performed |
|
||||
| negate | No | False | Negate (invert) the result of the condition's evaluation |
|
||||
|
||||
### Available Operations
|
||||
|
||||
* `eq`: Equals
|
||||
* `gt`: Greater than
|
||||
* `gte`: Greater than or equal to
|
||||
* `lt`: Less than
|
||||
* `lte`: Less than or equal to
|
||||
* `in`: Is present within a list of values
|
||||
* `contains`: Contains the specified value
|
||||
|
||||
### Accessing Nested Keys
|
||||
|
||||
To access nested keys, use dots to denote the path to the desired attribute. For example, assume the following data:
|
||||
|
||||
```json
|
||||
{
|
||||
"a": {
|
||||
"b": {
|
||||
"c": 123
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following condition will evaluate as true:
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "a.b.c",
|
||||
"value": 123
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
`name` equals "foo":
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "name",
|
||||
"value": "foo"
|
||||
}
|
||||
```
|
||||
|
||||
`name` does not equal "foo"
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "name",
|
||||
"value": "foo",
|
||||
"negate": true
|
||||
}
|
||||
```
|
||||
|
||||
`asn` is greater than 65000:
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "asn",
|
||||
"value": 65000,
|
||||
"op": "gt"
|
||||
}
|
||||
```
|
||||
|
||||
`status` is not "planned" or "staging":
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": ["planned", "staging"],
|
||||
"op": "in",
|
||||
"negate": true
|
||||
}
|
||||
```
|
||||
|
||||
!!! note "Evaluating static choice fields"
|
||||
Pay close attention when evaluating static choice fields, such as the `status` field above. These fields typically render as a dictionary specifying both the field's raw value (`value`) and its human-friendly label (`label`). be sure to specify on which of these you want to match.
|
||||
|
||||
## Condition Sets
|
||||
|
||||
Multiple conditions can be combined into nested sets using AND or OR logic. This is done by declaring a JSON object with a single key (`and` or `or`) containing a list of condition objects and/or child condition sets.
|
||||
|
||||
### Examples
|
||||
|
||||
`status` is "active" and `primary_ip` is defined _or_ the "exempt" tag is applied.
|
||||
|
||||
```json
|
||||
{
|
||||
"or": [
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"attr": "status.value",
|
||||
"value": "active"
|
||||
},
|
||||
{
|
||||
"attr": "primary_ip",
|
||||
"value": "",
|
||||
"negate": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"attr": "tags",
|
||||
"value": "exempt",
|
||||
"op": "contains"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
version-3.0.md
|
||||
121
docs/release-notes/index.md
Normal file
121
docs/release-notes/index.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Release Notes
|
||||
|
||||
NetBox releases are numbered as major, minor, and patch releases. For example, version 3.1.0 is a minor release, and v3.1.5 is a patch release. Briefly, these can be described as follows:
|
||||
|
||||
* **Major** - Introduces or removes an entire API or other core functionality
|
||||
* **Minor** - Implements major new features but may include breaking changes for API consumers or other integrations
|
||||
* **Patch** - A maintenance release which fixes bugs and may introduce backward-compatible enhancements
|
||||
|
||||
Minor releases are published in April, August, and December of each calendar year. Patch releases are published as needed to address bugs and fulfill minor feature requests, typically around every one to two weeks.
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.1](./version-3.1.md) (December 2021)
|
||||
|
||||
* Contact Objects ([#1344](https://github.com/netbox-community/netbox/issues/1344))
|
||||
* Wireless Networks ([#3979](https://github.com/netbox-community/netbox/issues/3979))
|
||||
* Dynamic Configuration Updates ([#5883](https://github.com/netbox-community/netbox/issues/5883))
|
||||
* First Hop Redundancy Protocol (FHRP) Groups ([#6235](https://github.com/netbox-community/netbox/issues/6235))
|
||||
* Conditional Webhooks ([#6238](https://github.com/netbox-community/netbox/issues/6238))
|
||||
* Interface Bridging ([#6346](https://github.com/netbox-community/netbox/issues/6346))
|
||||
* Multiple ASNs per Site ([#6732](https://github.com/netbox-community/netbox/issues/6732))
|
||||
* Single Sign-On (SSO) Authentication ([#7649](https://github.com/netbox-community/netbox/issues/7649))
|
||||
|
||||
#### [Version 3.0](./version-3.0.md) (August 2021)
|
||||
|
||||
* Updated User Interface ([#5893](https://github.com/netbox-community/netbox/issues/5893))
|
||||
* GraphQL API ([#2007](https://github.com/netbox-community/netbox/issues/2007))
|
||||
* IP Ranges ([#834](https://github.com/netbox-community/netbox/issues/834))
|
||||
* Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963))
|
||||
* SVG Cable Traces ([#6000](https://github.com/netbox-community/netbox/issues/6000))
|
||||
* New Views for Models Previously Under the Admin UI ([#6466](https://github.com/netbox-community/netbox/issues/6466))
|
||||
* REST API Token Provisioning ([#5264](https://github.com/netbox-community/netbox/issues/5264))
|
||||
* New Housekeeping Command ([#6590](https://github.com/netbox-community/netbox/issues/6590))
|
||||
* Custom Queue Support for Plugins ([#6651](https://github.com/netbox-community/netbox/issues/6651))
|
||||
|
||||
#### [Version 2.11](./version-2.11.md) (April 2021)
|
||||
|
||||
* Journaling Support ([#151](https://github.com/netbox-community/netbox/issues/151))
|
||||
* Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
|
||||
* Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451))
|
||||
* Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
|
||||
* Allow Assigning Devices to Locations ([#4971](https://github.com/netbox-community/netbox/issues/4971))
|
||||
* Dynamic Object Exports ([#4999](https://github.com/netbox-community/netbox/issues/4999))
|
||||
* Variable Scope Support for VLAN Groups ([#5284](https://github.com/netbox-community/netbox/issues/5284))
|
||||
* New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
|
||||
* Improved Change Logging ([#5913](https://github.com/netbox-community/netbox/issues/5913))
|
||||
* Provider Network Modeling ([#5986](https://github.com/netbox-community/netbox/issues/5986))
|
||||
|
||||
#### [Version 2.10](./version-2.10.md) (December 2020)
|
||||
|
||||
* Route Targets ([#259](https://github.com/netbox-community/netbox/issues/259))
|
||||
* REST API Bulk Deletion ([#3436](https://github.com/netbox-community/netbox/issues/3436))
|
||||
* REST API Bulk Update ([#4882](https://github.com/netbox-community/netbox/issues/4882))
|
||||
* Reimplementation of Custom Fields ([#4878](https://github.com/netbox-community/netbox/issues/4878))
|
||||
* Improved Cable Trace Performance ([#4900](https://github.com/netbox-community/netbox/issues/4900))
|
||||
|
||||
#### [Version 2.9](./version-2.9.md) (August 2020)
|
||||
|
||||
* Object-Based Permissions ([#554](https://github.com/netbox-community/netbox/issues/554))
|
||||
* Background Execution of Scripts & Reports ([#2006](https://github.com/netbox-community/netbox/issues/2006))
|
||||
* Named Virtual Chassis ([#2018](https://github.com/netbox-community/netbox/issues/2018))
|
||||
* Changes to Tag Creation ([#3703](https://github.com/netbox-community/netbox/issues/3703))
|
||||
* Dedicated Model for VM Interfaces ([#4721](https://github.com/netbox-community/netbox/issues/4721))
|
||||
* REST API Endpoints for Users and Groups ([#4877](https://github.com/netbox-community/netbox/issues/4877))
|
||||
|
||||
#### [Version 2.8](./version-2.8.md) (April 2020)
|
||||
|
||||
* Remote Authentication Support ([#2328](https://github.com/netbox-community/netbox/issues/2328))
|
||||
* Plugins ([#3351](https://github.com/netbox-community/netbox/issues/3351))
|
||||
|
||||
#### [Version 2.7](./version-2.7.md) (January 2020)
|
||||
|
||||
* Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451))
|
||||
* Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
|
||||
* External File Storage ([#1814](https://github.com/netbox-community/netbox/issues/1814))
|
||||
* Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
|
||||
|
||||
#### [Version 2.6](./version-2.6.md) (June 2019)
|
||||
|
||||
* Power Panels and Feeds ([#54](https://github.com/netbox-community/netbox/issues/54))
|
||||
* Caching ([#2647](https://github.com/netbox-community/netbox/issues/2647))
|
||||
* View Permissions ([#323](https://github.com/netbox-community/netbox/issues/323))
|
||||
* Custom Links ([#969](https://github.com/netbox-community/netbox/issues/969))
|
||||
* Prometheus Metrics ([#3104](https://github.com/netbox-community/netbox/issues/3104))
|
||||
|
||||
#### [Version 2.5](./version-2.5.md) (December 2018)
|
||||
|
||||
* Patch Panels and Cables ([#20](https://github.com/netbox-community/netbox/issues/20))
|
||||
|
||||
#### [Version 2.4](./version-2.4.md) (August 2018)
|
||||
|
||||
* Webhooks ([#81](https://github.com/netbox-community/netbox/issues/81))
|
||||
* Tagging ([#132](https://github.com/netbox-community/netbox/issues/132))
|
||||
* Contextual Configuration Data ([#1349](https://github.com/netbox-community/netbox/issues/1349))
|
||||
* Change Logging ([#1898](https://github.com/netbox-community/netbox/issues/1898))
|
||||
|
||||
#### [Version 2.3](./version-2.3.md) (February 2018)
|
||||
|
||||
* Virtual Chassis ([#99](https://github.com/netbox-community/netbox/issues/99))
|
||||
* Interface VLAN Assignments ([#150](https://github.com/netbox-community/netbox/issues/150))
|
||||
* Bulk Object Creation via the API ([#1553](https://github.com/netbox-community/netbox/issues/1553))
|
||||
* Automatic Provisioning of Next Available Prefixes ([#1694](https://github.com/netbox-community/netbox/issues/1694))
|
||||
* Bulk Renaming of Device/VM Components ([#1781](https://github.com/netbox-community/netbox/issues/1781))
|
||||
|
||||
#### [Version 2.2](./version-2.2.md) (October 2017)
|
||||
|
||||
* Virtual Machines and Clusters ([#142](https://github.com/netbox-community/netbox/issues/142))
|
||||
* Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511))
|
||||
|
||||
#### [Version 2.1](./version-2.1.md) (July 2017)
|
||||
|
||||
* IP Address Roles ([#819](https://github.com/netbox-community/netbox/issues/819))
|
||||
* Automatic Provisioning of Next Available IP ([#1246](https://github.com/netbox-community/netbox/issues/1246))
|
||||
* NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
|
||||
|
||||
#### [Version 2.0](./version-2.0.md) (May 2017)
|
||||
|
||||
* API 2.0 ([#113](https://github.com/netbox-community/netbox/issues/113))
|
||||
* Image Attachments ([#152](https://github.com/netbox-community/netbox/issues/152))
|
||||
* Global Search ([#159](https://github.com/netbox-community/netbox/issues/159))
|
||||
* Rack Elevations View ([#951](https://github.com/netbox-community/netbox/issues/951))
|
||||
@@ -1,5 +1,180 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## v3.0.12 (2021-12-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7751](https://github.com/netbox-community/netbox/issues/7751) - Get API user from LDAP only when `FIND_GROUP_PERMS` is enabled
|
||||
* [#7885](https://github.com/netbox-community/netbox/issues/7885) - Linkify VLAN name in VLANs table
|
||||
* [#7892](https://github.com/netbox-community/netbox/issues/7892) - Add L22-30 power port & outlet types
|
||||
* [#7932](https://github.com/netbox-community/netbox/issues/7932) - Improve performance of the "quick find" function
|
||||
* [#7941](https://github.com/netbox-community/netbox/issues/7941) - Add multi-standard ITA power outlet type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7823](https://github.com/netbox-community/netbox/issues/7823) - Fix issue where `return_url` is not honored when 'Save & Continue' button is present
|
||||
* [#7981](https://github.com/netbox-community/netbox/issues/7981) - Fix Markdown sanitization regex
|
||||
|
||||
---
|
||||
|
||||
## v3.0.11 (2021-11-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#2101](https://github.com/netbox-community/netbox/issues/2101) - Add missing `q` filters for necessary models
|
||||
* [#7424](https://github.com/netbox-community/netbox/issues/7424) - Add virtual chassis filters for device components
|
||||
* [#7531](https://github.com/netbox-community/netbox/issues/7531) - Add Markdown support for strikethrough formatting
|
||||
* [#7542](https://github.com/netbox-community/netbox/issues/7542) - Add optional VLAN group column to prefixes table
|
||||
* [#7803](https://github.com/netbox-community/netbox/issues/7803) - Improve live reloading of custom scripts
|
||||
* [#7810](https://github.com/netbox-community/netbox/issues/7810) - Add IEEE 802.15.1 interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7399](https://github.com/netbox-community/netbox/issues/7399) - Fix excessive CPU utilization when `AUTH_LDAP_FIND_GROUP_PERMS` is enabled
|
||||
* [#7657](https://github.com/netbox-community/netbox/issues/7657) - Make change logging middleware thread-safe
|
||||
* [#7720](https://github.com/netbox-community/netbox/issues/7720) - Fix initialization of custom script MultiObjectVar field with multiple values
|
||||
* [#7729](https://github.com/netbox-community/netbox/issues/7729) - Fix permissions evaluation when displaying VLAN group VLANs table
|
||||
* [#7739](https://github.com/netbox-community/netbox/issues/7739) - Fix exception when tracing cable across circuit with no far end termination
|
||||
* [#7813](https://github.com/netbox-community/netbox/issues/7813) - Fix handling of errors during export template rendering
|
||||
* [#7851](https://github.com/netbox-community/netbox/issues/7851) - Add missing cluster name filter for virtual machines
|
||||
* [#7857](https://github.com/netbox-community/netbox/issues/7857) - Fix ordering IP addresses by assignment status
|
||||
* [#7859](https://github.com/netbox-community/netbox/issues/7859) - Fix styling of form widgets under cable connection views
|
||||
* [#7864](https://github.com/netbox-community/netbox/issues/7864) - `power_port` can be null when creating power outlets via REST API
|
||||
* [#7865](https://github.com/netbox-community/netbox/issues/7865) - REST API should support null values for console port speeds
|
||||
|
||||
---
|
||||
|
||||
## v3.0.10 (2021-11-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7740](https://github.com/netbox-community/netbox/issues/7740) - Add mini-DIN 8 console port type
|
||||
* [#7760](https://github.com/netbox-community/netbox/issues/7760) - Add `vid` filter field to VLANs list
|
||||
* [#7767](https://github.com/netbox-community/netbox/issues/7767) - Add visual aids to interfaces table for type, enabled status
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7564](https://github.com/netbox-community/netbox/issues/7564) - Fix assignment of members to virtual chassis with initial position of zero
|
||||
* [#7701](https://github.com/netbox-community/netbox/issues/7701) - Fix conflation of assigned IP status & role in interface tables
|
||||
* [#7741](https://github.com/netbox-community/netbox/issues/7741) - Fix 404 when attaching multiple images in succession
|
||||
* [#7752](https://github.com/netbox-community/netbox/issues/7752) - Fix minimum version check under Python v3.10
|
||||
* [#7766](https://github.com/netbox-community/netbox/issues/7766) - Add missing outer dimension columns to rack table
|
||||
* [#7780](https://github.com/netbox-community/netbox/issues/7780) - Preserve multi-line values during CSV file import
|
||||
* [#7783](https://github.com/netbox-community/netbox/issues/7783) - Fix indentation of locations under site view
|
||||
* [#7788](https://github.com/netbox-community/netbox/issues/7788) - Improve XSS mitigation in Markdown renderer
|
||||
* [#7791](https://github.com/netbox-community/netbox/issues/7791) - Enable sorting device bays table by installed device status
|
||||
* [#7802](https://github.com/netbox-community/netbox/issues/7802) - Differentiate ID and VID columns in VLANs table
|
||||
* [#7808](https://github.com/netbox-community/netbox/issues/7808) - Fix reference values for content type under custom field import form
|
||||
* [#7809](https://github.com/netbox-community/netbox/issues/7809) - Add missing export template support for various models
|
||||
* [#7814](https://github.com/netbox-community/netbox/issues/7814) - Fix restriction of user & group objects in GraphQL API queries
|
||||
|
||||
---
|
||||
|
||||
## v3.0.9 (2021-11-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6529](https://github.com/netbox-community/netbox/issues/6529) - Introduce the `runscript` management command
|
||||
* [#6930](https://github.com/netbox-community/netbox/issues/6930) - Add an optional "ID" column to all tables
|
||||
* [#7668](https://github.com/netbox-community/netbox/issues/7668) - Add "view elevations" button to location view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7599](https://github.com/netbox-community/netbox/issues/7599) - Improve color mode preference handling
|
||||
* [#7601](https://github.com/netbox-community/netbox/issues/7601) - Correct devices count for locations within global search results
|
||||
* [#7612](https://github.com/netbox-community/netbox/issues/7612) - Strip HTML from custom field descriptions
|
||||
* [#7628](https://github.com/netbox-community/netbox/issues/7628) - Fix `load_yaml` method for custom scripts
|
||||
* [#7643](https://github.com/netbox-community/netbox/issues/7643) - Fix circuit assignment when creating multiple terminations simultaneously
|
||||
* [#7644](https://github.com/netbox-community/netbox/issues/7644) - Prevent inadvertent deletion of prior change records when deleting objects (#7333 revisited)
|
||||
* [#7647](https://github.com/netbox-community/netbox/issues/7647) - Require interface assignment when designating IP address as primary for device/VM during CSV import
|
||||
* [#7664](https://github.com/netbox-community/netbox/issues/7664) - Preserve initial form data when bulk edit validation fails
|
||||
* [#7717](https://github.com/netbox-community/netbox/issues/7717) - Restore missing tags column on IP range table
|
||||
* [#7721](https://github.com/netbox-community/netbox/issues/7721) - Retain pagination preference when `MAX_PAGE_SIZE` is zero
|
||||
|
||||
---
|
||||
|
||||
## v3.0.8 (2021-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
|
||||
* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
|
||||
* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
|
||||
* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
|
||||
* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
|
||||
* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
|
||||
* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
|
||||
* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
|
||||
* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
|
||||
* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
|
||||
|
||||
---
|
||||
|
||||
## v3.0.7 (2021-10-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6879](https://github.com/netbox-community/netbox/issues/6879) - Improve ability to toggle images/labels in rack elevations
|
||||
* [#7485](https://github.com/netbox-community/netbox/issues/7485) - Add USB micro AB type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7051](https://github.com/netbox-community/netbox/issues/7051) - Fix permissions evaluation and improve error handling for connected device REST API endpoint
|
||||
* [#7471](https://github.com/netbox-community/netbox/issues/7471) - Correct redirect URL when attaching images via "add another" button
|
||||
* [#7474](https://github.com/netbox-community/netbox/issues/7474) - Fix AttributeError exception when rendering a report or custom script
|
||||
* [#7479](https://github.com/netbox-community/netbox/issues/7479) - Fix parent interface choices when bulk editing VM interfaces
|
||||
|
||||
---
|
||||
|
||||
## v3.0.6 (2021-10-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6850](https://github.com/netbox-community/netbox/issues/6850) - Default to current user when creating journal entries via REST API
|
||||
* [#6955](https://github.com/netbox-community/netbox/issues/6955) - Include type, ID, and slug on object view
|
||||
* [#7394](https://github.com/netbox-community/netbox/issues/7394) - Enable filtering cables by termination type & ID in REST API
|
||||
* [#7462](https://github.com/netbox-community/netbox/issues/7462) - Include count of assigned virtual machines under platform view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7442](https://github.com/netbox-community/netbox/issues/7442) - Fix missing actions column on user-configured tables
|
||||
* [#7446](https://github.com/netbox-community/netbox/issues/7446) - Fix exception when viewing a large number of child IPs within a prefix
|
||||
* [#7455](https://github.com/netbox-community/netbox/issues/7455) - Fix site/provider network validation for circuit termination API serializer
|
||||
* [#7459](https://github.com/netbox-community/netbox/issues/7459) - Pre-populate location data when adding a device to a rack
|
||||
* [#7460](https://github.com/netbox-community/netbox/issues/7460) - Fix filtering connections by site ID
|
||||
|
||||
---
|
||||
|
||||
## v3.0.5 (2021-10-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5925](https://github.com/netbox-community/netbox/issues/5925) - Always show IP addresses tab under prefix view
|
||||
* [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications
|
||||
* [#6708](https://github.com/netbox-community/netbox/issues/6708) - Add image attachment support for circuits, power panels
|
||||
* [#7387](https://github.com/netbox-community/netbox/issues/7387) - Enable arbitrary ordering of custom scripts
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6433](https://github.com/netbox-community/netbox/issues/6433) - Fix bulk editing of child prefixes under aggregate view
|
||||
* [#6817](https://github.com/netbox-community/netbox/issues/6817) - Custom field columns should be removed from tables upon their deletion
|
||||
* [#6895](https://github.com/netbox-community/netbox/issues/6895) - Remove errant markup for null values in CSV export
|
||||
* [#7215](https://github.com/netbox-community/netbox/issues/7215) - Prevent rack elevations from overlapping when higher width is specified
|
||||
* [#7373](https://github.com/netbox-community/netbox/issues/7373) - Fix flashing when server, client, and browser color-mode preferences are mismatched
|
||||
* [#7397](https://github.com/netbox-community/netbox/issues/7397) - Fix AttributeError exception when rendering export template for devices via REST API
|
||||
* [#7401](https://github.com/netbox-community/netbox/issues/7401) - Pin `jsonschema` package to v3.2.0 to fix REST API docs rendering
|
||||
* [#7411](https://github.com/netbox-community/netbox/issues/7411) - Fix exception in UI when adding member devices to virtual chassis
|
||||
* [#7412](https://github.com/netbox-community/netbox/issues/7412) - Fix exception in UI when adding child device to device bay
|
||||
* [#7417](https://github.com/netbox-community/netbox/issues/7417) - Prevent exception when filtering objects list by invalid tag
|
||||
* [#7425](https://github.com/netbox-community/netbox/issues/7425) - Housekeeping command should honor zero verbosity
|
||||
* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
|
||||
|
||||
---
|
||||
|
||||
## v3.0.4 (2021-09-29)
|
||||
|
||||
### Enhancements
|
||||
@@ -30,6 +205,8 @@
|
||||
* [#7374](https://github.com/netbox-community/netbox/issues/7374) - Add missing `face` parameter to API elevations request when editing device
|
||||
* [#7392](https://github.com/netbox-community/netbox/issues/7392) - Fix "help" links for custom fields, other models
|
||||
|
||||
---
|
||||
|
||||
## v3.0.3 (2021-09-20)
|
||||
|
||||
### Enhancements
|
||||
@@ -190,7 +367,7 @@ More information about IP ranges is available [in the documentation](../models/i
|
||||
|
||||
#### Custom Model Validation ([#5963](https://github.com/netbox-community/netbox/issues/5963))
|
||||
|
||||
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/optional-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
|
||||
This release introduces the [`CUSTOM_VALIDATORS`](../configuration/dynamic-settings.md#custom_validators) configuration parameter, which allows administrators to map NetBox models to custom validator classes to enforce custom validation logic. For example, the following configuration requires every site to have a name of at least ten characters and a description:
|
||||
|
||||
```python
|
||||
from extras.validators import CustomValidator
|
||||
@@ -289,7 +466,7 @@ Note that NetBox's `rqworker` process will _not_ service custom queues by defaul
|
||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||
* [#6328](https://github.com/netbox-community/netbox/issues/6328) - Build and serve documentation locally
|
||||
|
||||
### Bug Fixes (from v3.2-beta2)
|
||||
### Bug Fixes (from v3.0-beta2)
|
||||
|
||||
* [#6977](https://github.com/netbox-community/netbox/issues/6977) - Truncate global search dropdown on small screens
|
||||
* [#6979](https://github.com/netbox-community/netbox/issues/6979) - Hide "create & add another" button for circuit terminations
|
||||
|
||||
391
docs/release-notes/version-3.1.md
Normal file
391
docs/release-notes/version-3.1.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# NetBox v3.1
|
||||
|
||||
## v3.1.8 (2022-02-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
|
||||
* [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content
|
||||
* [#8556](https://github.com/netbox-community/netbox/issues/8556) - Add full username column to changelog table
|
||||
* [#8620](https://github.com/netbox-community/netbox/issues/8620) - Enable tab completion for `nbshell`
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
|
||||
* [#8391](https://github.com/netbox-community/netbox/issues/8391) - Null date columns should return empty strings during CSV export
|
||||
* [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero
|
||||
* [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port
|
||||
* [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns`
|
||||
* [#8577](https://github.com/netbox-community/netbox/issues/8577) - Show contact assignment counts in global search results
|
||||
* [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences
|
||||
* [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form
|
||||
* [#8609](https://github.com/netbox-community/netbox/issues/8609) - Display validation error when attempting to assign VLANs to interface with no mode during bulk edit
|
||||
* [#8611](https://github.com/netbox-community/netbox/issues/8611) - Fix bulk editing for certain custom link, webhook, and journal entry fields
|
||||
|
||||
---
|
||||
|
||||
## v3.1.7 (2022-02-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7504](https://github.com/netbox-community/netbox/issues/7504) - Include IP range data under IPAM role views
|
||||
* [#8275](https://github.com/netbox-community/netbox/issues/8275) - Introduce alternative ASDOT-formatted column for ASNs
|
||||
* [#8367](https://github.com/netbox-community/netbox/issues/8367) - Add ASNs to global search function
|
||||
* [#8368](https://github.com/netbox-community/netbox/issues/8368) - Enable controlling the order of custom script form fields with `field_order`
|
||||
* [#8381](https://github.com/netbox-community/netbox/issues/8381) - Add contacts to global search function
|
||||
* [#8462](https://github.com/netbox-community/netbox/issues/8462) - Linkify manufacturer column in device type table
|
||||
* [#8476](https://github.com/netbox-community/netbox/issues/8476) - Bring the ASN Web UI up to the standard set by other objects
|
||||
* [#8494](https://github.com/netbox-community/netbox/issues/8494) - Include locations count under tenant view
|
||||
* [#8517](https://github.com/netbox-community/netbox/issues/8517) - Render boolean custom fields as icons in object tables
|
||||
* [#8530](https://github.com/netbox-community/netbox/issues/8530) - Indicate CSV or YAML as format for "all data" export
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8315](https://github.com/netbox-community/netbox/issues/8315) - Fix display of NAT link for primary IPv4 address under device view
|
||||
* [#8377](https://github.com/netbox-community/netbox/issues/8377) - Fix calculation of absolute cable lengths when specified in fractional units
|
||||
* [#8425](https://github.com/netbox-community/netbox/issues/8425) - Fix exception when viewing change list/records with removed plugins
|
||||
* [#8456](https://github.com/netbox-community/netbox/issues/8456) - Fix redundant display of VRF RD in prefix view
|
||||
* [#8465](https://github.com/netbox-community/netbox/issues/8465) - Accept empty string values for Interface `rf_channel` in REST API
|
||||
* [#8498](https://github.com/netbox-community/netbox/issues/8498) - Fix display of selected content type filters in object list views
|
||||
* [#8499](https://github.com/netbox-community/netbox/issues/8499) - Content types REST API endpoint should not require model permission
|
||||
* [#8512](https://github.com/netbox-community/netbox/issues/8512) - Correct file permissions to allow execution of housekeeping script
|
||||
* [#8527](https://github.com/netbox-community/netbox/issues/8527) - Fix display of changelog retention period
|
||||
|
||||
---
|
||||
|
||||
## v3.1.6 (2022-01-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8246](https://github.com/netbox-community/netbox/issues/8246) - Show human-friendly values for commit rates in circuits table
|
||||
* [#8262](https://github.com/netbox-community/netbox/issues/8262) - Add cable count to tenant stats
|
||||
* [#8265](https://github.com/netbox-community/netbox/issues/8265) - Add Stackwise-n interface types
|
||||
* [#8293](https://github.com/netbox-community/netbox/issues/8293) - Show 4-byte ASNs in ASDOT notation
|
||||
* [#8302](https://github.com/netbox-community/netbox/issues/8302) - Linkify role column in device & VM tables
|
||||
* [#8337](https://github.com/netbox-community/netbox/issues/8337) - Enable sorting object tables by created & updated times
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8279](https://github.com/netbox-community/netbox/issues/8279) - Fix display of virtual chassis members in rack elevations
|
||||
* [#8285](https://github.com/netbox-community/netbox/issues/8285) - Fix `cluster_count` under tenant REST API serializer
|
||||
* [#8287](https://github.com/netbox-community/netbox/issues/8287) - Correct label in export template form
|
||||
* [#8301](https://github.com/netbox-community/netbox/issues/8301) - Fix delete button for various object children views
|
||||
* [#8305](https://github.com/netbox-community/netbox/issues/8305) - Fix assignment of custom field data to FHRP groups via UI
|
||||
* [#8306](https://github.com/netbox-community/netbox/issues/8306) - Redirect user to previous page after login
|
||||
* [#8314](https://github.com/netbox-community/netbox/issues/8314) - Prevent custom fields with default values from appearing as applied filters erroneously
|
||||
* [#8317](https://github.com/netbox-community/netbox/issues/8317) - Fix CSV import of multi-select custom field values
|
||||
* [#8319](https://github.com/netbox-community/netbox/issues/8319) - Custom URL fields should honor `ALLOWED_URL_SCHEMES` config parameter
|
||||
* [#8342](https://github.com/netbox-community/netbox/issues/8342) - Restore `created` & `last_updated` fields missing from several REST API serializers
|
||||
* [#8357](https://github.com/netbox-community/netbox/issues/8357) - Add missing tags field to location filter form
|
||||
* [#8358](https://github.com/netbox-community/netbox/issues/8358) - Fix inconsistent styling of custom fields on filter & bulk edit forms
|
||||
|
||||
---
|
||||
|
||||
## v3.1.5 (2022-01-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
|
||||
* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
|
||||
* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
|
||||
* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
|
||||
* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
|
||||
* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
|
||||
* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
|
||||
|
||||
---
|
||||
|
||||
## v3.1.4 (2022-01-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8192](https://github.com/netbox-community/netbox/issues/8192) - Add "add prefix" button to aggregate child prefixes view
|
||||
* [#8194](https://github.com/netbox-community/netbox/issues/8194) - Enable bulk user assignment to groups under admin UI
|
||||
* [#8197](https://github.com/netbox-community/netbox/issues/8197) - Allow filtering sites by group when connecting a cable
|
||||
* [#8210](https://github.com/netbox-community/netbox/issues/8210) - Establish `netbox/local/` as a path for local resources
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#8187](https://github.com/netbox-community/netbox/issues/8187) - Fix rendering of tags column in object tables
|
||||
* [#8191](https://github.com/netbox-community/netbox/issues/8191) - Fix return URL when adding IP addresses to VM interfaces
|
||||
* [#8196](https://github.com/netbox-community/netbox/issues/8196) - Fix IndexError exception when viewing large IPv6 prefixes in UI
|
||||
* [#8201](https://github.com/netbox-community/netbox/issues/8201) - Custom integer fields should allow negative integers as minimum/maximum values
|
||||
|
||||
---
|
||||
|
||||
## v3.1.3 (2021-12-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
|
||||
* [#7600](https://github.com/netbox-community/netbox/issues/7600) - Include count of available IPs on prefix view
|
||||
* [#8034](https://github.com/netbox-community/netbox/issues/8034) - Enable specifying custom field validators during CSV import
|
||||
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
|
||||
* [#8175](https://github.com/netbox-community/netbox/issues/8175) - Display parent object when attaching an image
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads
|
||||
* [#7290](https://github.com/netbox-community/netbox/issues/7290) - Defer loading API-backed form fields
|
||||
* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts
|
||||
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
|
||||
* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger
|
||||
* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables
|
||||
* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view
|
||||
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
|
||||
* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables
|
||||
* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view
|
||||
* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers
|
||||
|
||||
---
|
||||
|
||||
## v3.1.2 (2021-12-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7661](https://github.com/netbox-community/netbox/issues/7661) - Remove forced styling of custom banners
|
||||
* [#7665](https://github.com/netbox-community/netbox/issues/7665) - Add toggle to show only available child prefixes
|
||||
* [#7999](https://github.com/netbox-community/netbox/issues/7999) - Add 6 GHz and 60 GHz wireless channels
|
||||
* [#8057](https://github.com/netbox-community/netbox/issues/8057) - Dynamic object tables using HTMX
|
||||
* [#8080](https://github.com/netbox-community/netbox/issues/8080) - Link to NAT IPs for device/VM primary IPs
|
||||
* [#8081](https://github.com/netbox-community/netbox/issues/8081) - Allow creating services directly from navigation menu
|
||||
* [#8083](https://github.com/netbox-community/netbox/issues/8083) - Removed "related devices" panel from device view
|
||||
* [#8108](https://github.com/netbox-community/netbox/issues/8108) - Improve breadcrumb links for device/VM components
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7674](https://github.com/netbox-community/netbox/issues/7674) - Fix inadvertent application of device type context to virtual machines
|
||||
* [#8074](https://github.com/netbox-community/netbox/issues/8074) - Ordering VMs by name should reference naturalized value
|
||||
* [#8077](https://github.com/netbox-community/netbox/issues/8077) - Fix exception when attaching image to location, circuit, or power panel
|
||||
* [#8078](https://github.com/netbox-community/netbox/issues/8078) - Add missing wireless models to `lsmodels()` in `nbshell`
|
||||
* [#8079](https://github.com/netbox-community/netbox/issues/8079) - Fix validation of LLDP neighbors when connected device has an asset tag
|
||||
* [#8088](https://github.com/netbox-community/netbox/issues/8088) - Improve legibility of text in labels with light-colored backgrounds
|
||||
* [#8092](https://github.com/netbox-community/netbox/issues/8092) - Rack elevations should not include device asset tags
|
||||
* [#8096](https://github.com/netbox-community/netbox/issues/8096) - Fix DataError during change logging of objects with very long string representations
|
||||
* [#8101](https://github.com/netbox-community/netbox/issues/8101) - Preserve return URL when using "create and add another" button
|
||||
* [#8102](https://github.com/netbox-community/netbox/issues/8102) - Raise validation error when attempting to assign an IP address to multiple objects
|
||||
|
||||
---
|
||||
|
||||
## v3.1.1 (2021-12-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8047](https://github.com/netbox-community/netbox/issues/8047) - Display sorting indicator in table column headers
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5869](https://github.com/netbox-community/netbox/issues/5869) - Fix permissions evaluation under available prefix/IP REST API endpoints
|
||||
* [#7519](https://github.com/netbox-community/netbox/issues/7519) - Return a 409 status for unfulfillable available prefix/IP requests
|
||||
* [#7690](https://github.com/netbox-community/netbox/issues/7690) - Fix custom field integer support for MultiValueNumberFilter
|
||||
* [#7990](https://github.com/netbox-community/netbox/issues/7990) - Fix `title` display on contact detail view
|
||||
* [#7996](https://github.com/netbox-community/netbox/issues/7996) - Show WWN field in interface creation form
|
||||
* [#8001](https://github.com/netbox-community/netbox/issues/8001) - Correct verbose name for wireless LAN group model
|
||||
* [#8003](https://github.com/netbox-community/netbox/issues/8003) - Fix cable tracing across bridged interfaces with no cable
|
||||
* [#8005](https://github.com/netbox-community/netbox/issues/8005) - Fix contact email display
|
||||
* [#8009](https://github.com/netbox-community/netbox/issues/8009) - Validate IP addresses for uniqueness when creating an FHRP group
|
||||
* [#8010](https://github.com/netbox-community/netbox/issues/8010) - Allow filtering devices by multiple serial numbers
|
||||
* [#8019](https://github.com/netbox-community/netbox/issues/8019) - Exclude metrics endpoint when `LOGIN_REQUIRED` is true
|
||||
* [#8030](https://github.com/netbox-community/netbox/issues/8030) - Validate custom field names
|
||||
* [#8033](https://github.com/netbox-community/netbox/issues/8033) - Fix display of zero values for custom integer fields in tables
|
||||
* [#8035](https://github.com/netbox-community/netbox/issues/8035) - Redirect back to parent prefix after creating IP address(es) where applicable
|
||||
* [#8038](https://github.com/netbox-community/netbox/issues/8038) - Placeholder filter should display zero integer values
|
||||
* [#8042](https://github.com/netbox-community/netbox/issues/8042) - Fix filtering cables list by site slug or rack name
|
||||
* [#8051](https://github.com/netbox-community/netbox/issues/8051) - Contact group parent assignment should not be required under REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.1.0 (2021-12-06)
|
||||
|
||||
!!! warning "PostgreSQL 10 Required"
|
||||
NetBox v3.1 requires PostgreSQL 10 or later.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination.
|
||||
* The `cable_peer` and `cable_peer_type` attributes of cable termination models have been renamed to `link_peer` and `link_peer_type`, respectively, to accommodate wireless links between interfaces.
|
||||
* Exported webhooks and custom fields now reference associated content types by raw string value (e.g. "dcim.site") rather than by human-friendly name.
|
||||
* The 128GFC interface type has been corrected from `128gfc-sfp28` to `128gfc-qsfp28`.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Contact Objects ([#1344](https://github.com/netbox-community/netbox/issues/1344))
|
||||
|
||||
A set of new models for tracking contact information has been introduced within the tenancy app. Users may now create individual contact objects to be associated with various models within NetBox. Each contact has a name, title, email address, etc. Contacts can be arranged in hierarchical groups for ease of management.
|
||||
|
||||
When assigning a contact to an object, the user must select a predefined role (e.g. "billing" or "technical") and may optionally indicate a priority relative to other contacts associated with the object. There is no limit on how many contacts can be assigned to an object, nor on how many objects to which a contact can be assigned.
|
||||
|
||||
#### Wireless Networks ([#3979](https://github.com/netbox-community/netbox/issues/3979))
|
||||
|
||||
This release introduces two new models to represent wireless networks:
|
||||
|
||||
* Wireless LAN - A multi-access wireless segment to which any number of wireless interfaces may be attached
|
||||
* Wireless Link - A point-to-point connection between exactly two wireless interfaces
|
||||
|
||||
Both types of connection include SSID and authentication attributes. Additionally, the interface model has been extended to include several attributes pertinent to wireless operation:
|
||||
|
||||
* Wireless role - Access point or station
|
||||
* Channel - A predefined channel within a standardized band
|
||||
* Channel frequency & width - Customizable channel attributes (e.g. for licensed bands)
|
||||
|
||||
#### Dynamic Configuration Updates ([#5883](https://github.com/netbox-community/netbox/issues/5883))
|
||||
|
||||
Some parameters of NetBox's configuration are now accessible via the admin UI. These parameters can be modified by an administrator and take effect immediately upon application: There is no need to restart NetBox. Additionally, each iteration of the dynamic configuration is preserved in the database, and can be restored by an administrator at any time.
|
||||
|
||||
Dynamic configuration parameters may also still be defined within `configuration.py`, and the settings defined here take precedence over those defined via the user interface.
|
||||
|
||||
For a complete list of supported parameters, please see the [dynamic configuration documentation](../configuration/dynamic-settings.md).
|
||||
|
||||
#### First Hop Redundancy Protocol (FHRP) Groups ([#6235](https://github.com/netbox-community/netbox/issues/6235))
|
||||
|
||||
A new FHRP group model has been introduced to aid in modeling the configurations of protocols such as HSRP, VRRP, and GLBP. Each FHRP group may be assigned one or more virtual IP addresses, as well as an authentication type and key. Member device and VM interfaces may be associated with one or more FHRP groups, with each assignment receiving a numeric priority designation.
|
||||
|
||||
#### Conditional Webhooks ([#6238](https://github.com/netbox-community/netbox/issues/6238))
|
||||
|
||||
Webhooks now include a `conditions` field, which may be used to specify conditions under which a webhook triggers. For example, you may wish to generate outgoing requests for a device webhook only when its status is "active" or "staged". This can be done by declaring conditional logic in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"attr": "status.value",
|
||||
"op": "in",
|
||||
"value": ["active", "staged"]
|
||||
}
|
||||
```
|
||||
|
||||
Multiple conditions may be nested using AND/OR logic as well. For more information, please see the [conditional logic documentation](../reference/conditions.md).
|
||||
|
||||
#### Interface Bridging ([#6346](https://github.com/netbox-community/netbox/issues/6346))
|
||||
|
||||
A `bridge` field has been added to the interface model for devices and virtual machines. This can be set to reference another interface on the same parent device/VM to indicate a direct layer two bridging adjacency. Additionally, "bridge" has been added as an interface type. (However, interfaces of any type may be designated as bridged.)
|
||||
|
||||
Multiple interfaces can be bridged to a single virtual interface to effect a bridge group. Alternatively, two physical interfaces can be bridged to one another, to effect an internal cross-connect.
|
||||
|
||||
#### Multiple ASNs per Site ([#6732](https://github.com/netbox-community/netbox/issues/6732))
|
||||
|
||||
With the introduction of the new ASN model, NetBox now supports the assignment of multiple ASNs per site. Each ASN instance must have a 32-bit AS number, and may optionally be assigned to a RIR and/or Tenant.
|
||||
|
||||
The `asn` integer field on the site model has been preserved to maintain backward compatability until a later release.
|
||||
|
||||
#### Single Sign-On (SSO) Authentication ([#7649](https://github.com/netbox-community/netbox/issues/7649))
|
||||
|
||||
Support for single sign-on (SSO) authentication has been added via the [python-social-auth](https://github.com/python-social-auth) library. NetBox administrators can configure one of the [supported authentication backends](https://python-social-auth.readthedocs.io/en/latest/intro.html#auth-providers) to enable SSO authentication for users.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
||||
* [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names
|
||||
* [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices
|
||||
* [#5143](https://github.com/netbox-community/netbox/issues/5143) - Include a device's asset tag in its display value
|
||||
* [#6497](https://github.com/netbox-community/netbox/issues/6497) - Extend tag support to organizational models
|
||||
* [#6615](https://github.com/netbox-community/netbox/issues/6615) - Add filter lookups for custom fields
|
||||
* [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support
|
||||
* [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables
|
||||
* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
|
||||
* [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names
|
||||
* [#7452](https://github.com/netbox-community/netbox/issues/7452) - Add `json` custom field type
|
||||
* [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views
|
||||
* [#7606](https://github.com/netbox-community/netbox/issues/7606) - Model transmit power for interfaces
|
||||
* [#7619](https://github.com/netbox-community/netbox/issues/7619) - Permit custom validation rules to be defined as plain data or dotted path to class
|
||||
* [#7761](https://github.com/netbox-community/netbox/issues/7761) - Extend cable tracing across bridged interfaces
|
||||
* [#7812](https://github.com/netbox-community/netbox/issues/7812) - Enable change logging for image attachments
|
||||
* [#7858](https://github.com/netbox-community/netbox/issues/7858) - Standardize the representation of content types across import & export functions
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7589](https://github.com/netbox-community/netbox/issues/7589) - Correct 128GFC interface type identifier
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* Added the following endpoints for ASNs:
|
||||
* `/api/ipam/asn/`
|
||||
* Added the following endpoints for FHRP groups:
|
||||
* `/api/ipam/fhrp-groups/`
|
||||
* `/api/ipam/fhrp-group-assignments/`
|
||||
* Added the following endpoints for contacts:
|
||||
* `/api/tenancy/contact-assignments/`
|
||||
* `/api/tenancy/contact-groups/`
|
||||
* `/api/tenancy/contact-roles/`
|
||||
* `/api/tenancy/contacts/`
|
||||
* Added the following endpoints for wireless networks:
|
||||
* `/api/wireless/wireless-lans/`
|
||||
* `/api/wireless/wireless-lan-groups/`
|
||||
* `/api/wireless/wireless-links/`
|
||||
* Added `tags` field to the following models:
|
||||
* circuits.CircuitType
|
||||
* dcim.DeviceRole
|
||||
* dcim.Location
|
||||
* dcim.Manufacturer
|
||||
* dcim.Platform
|
||||
* dcim.RackRole
|
||||
* dcim.Region
|
||||
* dcim.SiteGroup
|
||||
* ipam.RIR
|
||||
* ipam.Role
|
||||
* ipam.VLANGroup
|
||||
* tenancy.ContactGroup
|
||||
* tenancy.ContactRole
|
||||
* tenancy.TenantGroup
|
||||
* virtualization.ClusterGroup
|
||||
* virtualization.ClusterType
|
||||
* circuits.CircuitTermination
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Cable
|
||||
* Added `tenant` field
|
||||
* dcim.ConsolePort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.ConsoleServerPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Device
|
||||
* The `display` field now includes the device's asset tag, if set
|
||||
* Added `airflow` field
|
||||
* dcim.DeviceType
|
||||
* Added `airflow` field
|
||||
* dcim.FrontPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Interface
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* Added `bridge` field
|
||||
* Added `rf_channel` field
|
||||
* Added `rf_channel_frequency` field
|
||||
* Added `rf_channel_width` field
|
||||
* Added `rf_role` field
|
||||
* Added `tx_power` field
|
||||
* Added `wireless_link` field
|
||||
* Added `wwn` field
|
||||
* Added `count_fhrp_groups` read-only field
|
||||
* dcim.Location
|
||||
* Added `tenant` field
|
||||
* dcim.PowerFeed
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.PowerOutlet
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.PowerPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.RearPort
|
||||
* `cable_peer` has been renamed to `link_peer`
|
||||
* `cable_peer_type` has been renamed to `link_peer_type`
|
||||
* dcim.Site
|
||||
* Added `asns` relationship to ipam.ASN
|
||||
* extras.ImageAttachment
|
||||
* Added the `last_updated` field
|
||||
* extras.Webhook
|
||||
* Added the `conditions` field
|
||||
* virtualization.VMInterface
|
||||
* Added `bridge` field
|
||||
* Added `count_fhrp_groups` read-only field
|
||||
@@ -42,7 +42,7 @@ $ curl -X POST \
|
||||
https://netbox/api/users/tokens/provision/ \
|
||||
--data '{
|
||||
"username": "hankhill",
|
||||
"password: "I<3C3H8",
|
||||
"password": "I<3C3H8",
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ Vary: Accept
|
||||
}
|
||||
```
|
||||
|
||||
The default page is determined by the [`PAGINATE_COUNT`](../configuration/optional-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
The default page is determined by the [`PAGINATE_COUNT`](../configuration/dynamic-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
|
||||
```
|
||||
http://netbox/api/dcim/devices/?limit=100
|
||||
@@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/dynamic-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
|
||||
!!! warning
|
||||
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
||||
|
||||
11
mkdocs.yml
11
mkdocs.yml
@@ -51,6 +51,8 @@ nav:
|
||||
- Configuring NetBox: 'configuration/index.md'
|
||||
- Required Settings: 'configuration/required-settings.md'
|
||||
- Optional Settings: 'configuration/optional-settings.md'
|
||||
- Dynamic Settings: 'configuration/dynamic-settings.md'
|
||||
- Remote Authentication: 'configuration/remote-authentication.md'
|
||||
- Core Functionality:
|
||||
- IP Address Management: 'core-functionality/ipam.md'
|
||||
- VLAN Management: 'core-functionality/vlans.md'
|
||||
@@ -60,12 +62,14 @@ nav:
|
||||
- Virtualization: 'core-functionality/virtualization.md'
|
||||
- Service Mapping: 'core-functionality/services.md'
|
||||
- Circuits: 'core-functionality/circuits.md'
|
||||
- Wireless: 'core-functionality/wireless.md'
|
||||
- Power Tracking: 'core-functionality/power.md'
|
||||
- Tenancy: 'core-functionality/tenancy.md'
|
||||
- Contacts: 'core-functionality/contacts.md'
|
||||
- Customization:
|
||||
- Custom Fields: 'customization/custom-fields.md'
|
||||
- Custom Validation: 'customization/custom-validation.md'
|
||||
- Custom Links: 'customization/custom-links.md'
|
||||
- Custom Links: 'models/extras/customlink.md'
|
||||
- Export Templates: 'customization/export-templates.md'
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Reports: 'customization/reports.md'
|
||||
@@ -81,6 +85,7 @@ nav:
|
||||
- Using Plugins: 'plugins/index.md'
|
||||
- Developing Plugins: 'plugins/development.md'
|
||||
- Administration:
|
||||
- Authentication: 'administration/authentication.md'
|
||||
- Permissions: 'administration/permissions.md'
|
||||
- Housekeeping: 'administration/housekeeping.md'
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
@@ -91,6 +96,8 @@ nav:
|
||||
- Authentication: 'rest-api/authentication.md'
|
||||
- GraphQL API:
|
||||
- Overview: 'graphql-api/overview.md'
|
||||
- Reference:
|
||||
- Conditions: 'reference/conditions.md'
|
||||
- Development:
|
||||
- Introduction: 'development/index.md'
|
||||
- Getting Started: 'development/getting-started.md'
|
||||
@@ -104,6 +111,8 @@ nav:
|
||||
- Web UI: 'development/web-ui.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 3.1: 'release-notes/version-3.1.md'
|
||||
- Version 3.0: 'release-notes/version-3.0.md'
|
||||
- Version 2.11: 'release-notes/version-2.11.md'
|
||||
- Version 2.10: 'release-notes/version-2.10.md'
|
||||
|
||||
@@ -3,11 +3,9 @@ from rest_framework import serializers
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||
from dcim.api.serializers import LinkTerminationSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||
)
|
||||
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class CircuitTypeSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
]
|
||||
|
||||
@@ -90,17 +88,17 @@ class CircuitSerializer(PrimaryModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
|
||||
class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer(required=False)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'_occupied',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||
'_occupied', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
|
||||
@@ -111,6 +111,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
|
||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
|
||||
@@ -4,9 +4,7 @@ from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect,
|
||||
)
|
||||
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@@ -16,7 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -55,7 +53,7 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -79,7 +77,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -93,7 +91,7 @@ class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
|
||||
@@ -6,7 +6,7 @@ from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.forms import CustomFieldModelFilterForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
@@ -16,29 +16,22 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ProviderFilterForm(CustomFieldModelFilterForm):
|
||||
model = Provider
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['asn'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -47,8 +40,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
@@ -57,39 +49,26 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ProviderNetworkFilterForm(CustomFieldModelFilterForm):
|
||||
model = ProviderNetwork
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('provider_id',),
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class CircuitTypeFilterForm(CustomFieldModelFilterForm):
|
||||
model = CircuitType
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Circuit
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
@@ -98,22 +77,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
label=_('Type'),
|
||||
fetch_trigger='open'
|
||||
label=_('Type')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
@@ -121,8 +93,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network'),
|
||||
fetch_trigger='open'
|
||||
label=_('Provider network')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
@@ -132,14 +103,12 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -148,8 +117,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
|
||||
@@ -19,7 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ProviderForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@@ -53,7 +53,7 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ProviderNetworkForm(CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
@@ -73,17 +73,21 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class CircuitTypeForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'description',
|
||||
'name', 'slug', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class CircuitForm(TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
|
||||
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal file
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0062_clear_secrets_changelog'),
|
||||
('circuits', '0002_squashed_0029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||
),
|
||||
]
|
||||
21
netbox/circuits/migrations/0004_rename_cable_peer.py
Normal file
21
netbox/circuits/migrations/0004_rename_cable_peer.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0003_extend_tag_support'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_id',
|
||||
new_name='_link_peer_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='_cable_peer_type',
|
||||
new_name='_link_peer_type',
|
||||
),
|
||||
]
|
||||
2
netbox/circuits/models/__init__.py
Normal file
2
netbox/circuits/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .circuits import *
|
||||
from .providers import *
|
||||
@@ -1,128 +1,21 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination, PathEndpoint
|
||||
from extras.models import ObjectChange
|
||||
from circuits.choices import *
|
||||
from dcim.models import LinkTermination
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from .choices import *
|
||||
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
'CircuitTermination',
|
||||
'CircuitType',
|
||||
'ProviderNetwork',
|
||||
'Provider',
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Provider(PrimaryModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
stores information pertinent to the user's relationship with the Provider.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
asn = ASNField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
verbose_name='Account number'
|
||||
)
|
||||
portal_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Portal URL'
|
||||
)
|
||||
noc_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='NOC contact'
|
||||
)
|
||||
admin_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='Admin contact'
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
unimportant to the user.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='networks'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'name')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_providernetwork_provider_name'
|
||||
),
|
||||
)
|
||||
unique_together = ('provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class CircuitType(OrganizationalModel):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
@@ -141,8 +34,6 @@ class CircuitType(OrganizationalModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -203,6 +94,14 @@ class Circuit(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
# Cache associated CircuitTerminations
|
||||
termination_a = models.ForeignKey(
|
||||
to='circuits.CircuitTermination',
|
||||
@@ -221,8 +120,6 @@ class Circuit(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
]
|
||||
@@ -242,7 +139,7 @@ class Circuit(PrimaryModel):
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||
class CircuitTermination(ChangeLoggedModel, LinkTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
@@ -261,7 +158,7 @@ class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to=ProviderNetwork,
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
@@ -293,8 +190,6 @@ class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
112
netbox/circuits/models/providers.py
Normal file
112
netbox/circuits/models/providers.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from extras.utils import extras_features
|
||||
from netbox.models import PrimaryModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'ProviderNetwork',
|
||||
'Provider',
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Provider(PrimaryModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
stores information pertinent to the user's relationship with the Provider.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
asn = ASNField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='ASN',
|
||||
help_text='32-bit autonomous system number'
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
verbose_name='Account number'
|
||||
)
|
||||
portal_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Portal URL'
|
||||
)
|
||||
noc_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='NOC contact'
|
||||
)
|
||||
admin_contact = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='Admin contact'
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
unimportant to the user.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='networks'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'name')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_providernetwork_provider_name'
|
||||
),
|
||||
)
|
||||
unique_together = ('provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
@@ -11,6 +11,7 @@ def update_circuit(instance, **kwargs):
|
||||
When a CircuitTermination has been modified, update its parent Circuit.
|
||||
"""
|
||||
termination_name = f'termination_{instance.term_side.lower()}'
|
||||
instance.circuit.refresh_from_db()
|
||||
setattr(instance.circuit, termination_name, instance)
|
||||
instance.circuit.save()
|
||||
|
||||
|
||||
@@ -22,11 +22,32 @@ CIRCUITTERMINATION_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
#
|
||||
# Table columns
|
||||
#
|
||||
|
||||
|
||||
class CommitRateColumn(tables.TemplateColumn):
|
||||
"""
|
||||
Humanize the commit rate in the column view
|
||||
"""
|
||||
|
||||
template_code = """
|
||||
{% load helpers %}
|
||||
{{ record.commit_rate|humanize_speed }}
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(template_code=self.template_code, *args, **kwargs)
|
||||
|
||||
def value(self, value):
|
||||
return str(value) if value else None
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@@ -44,8 +65,8 @@ class ProviderTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'comments',
|
||||
'tags',
|
||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
@@ -69,7 +90,7 @@ class ProviderNetworkTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ProviderNetwork
|
||||
fields = ('pk', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
@@ -82,6 +103,9 @@ class CircuitTypeTable(BaseTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuittype_list'
|
||||
)
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
@@ -89,7 +113,7 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
@@ -101,7 +125,7 @@ class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
verbose_name='Circuit ID'
|
||||
)
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
@@ -116,6 +140,7 @@ class CircuitTable(BaseTable):
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
verbose_name='Side Z'
|
||||
)
|
||||
commit_rate = CommitRateColumn()
|
||||
comments = MarkdownColumn()
|
||||
tags = TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
@@ -124,8 +149,8 @@ class CircuitTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags',
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
||||
@@ -136,14 +136,20 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
SIDE_A = CircuitTerminationSideChoices.SIDE_A
|
||||
SIDE_Z = CircuitTerminationSideChoices.SIDE_Z
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
|
||||
@@ -153,10 +159,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@@ -164,13 +170,13 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[1].pk,
|
||||
'site': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'site': sites[1].pk,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -64,10 +64,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
])
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Circuit Type X',
|
||||
'slug': 'circuit-type-x',
|
||||
'description': 'A new circuit type',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
|
||||
@@ -340,7 +340,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import VLAN
|
||||
from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer
|
||||
from ipam.models import ASN, VLAN
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import (
|
||||
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from netbox.config import ConfigItem
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
|
||||
from wireless.choices import *
|
||||
from wireless.models import WirelessLAN
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
class CableTerminationSerializer(serializers.ModelSerializer):
|
||||
cable_peer_type = serializers.SerializerMethodField(read_only=True)
|
||||
cable_peer = serializers.SerializerMethodField(read_only=True)
|
||||
class LinkTerminationSerializer(serializers.ModelSerializer):
|
||||
link_peer_type = serializers.SerializerMethodField(read_only=True)
|
||||
link_peer = serializers.SerializerMethodField(read_only=True)
|
||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_cable_peer_type(self, obj):
|
||||
if obj._cable_peer is not None:
|
||||
return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}'
|
||||
def get_link_peer_type(self, obj):
|
||||
if obj._link_peer is not None:
|
||||
return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}'
|
||||
return None
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_cable_peer(self, obj):
|
||||
def get_link_peer(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the cable termination model.
|
||||
Return the appropriate serializer for the link termination model.
|
||||
"""
|
||||
if obj._cable_peer is not None:
|
||||
serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested')
|
||||
if obj._link_peer is not None:
|
||||
serializer = get_serializer_for_model(obj._link_peer, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj._cable_peer, context=context).data
|
||||
return serializer(obj._link_peer, context=context).data
|
||||
return None
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||
@@ -82,27 +83,27 @@ class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
||||
|
||||
class RegionSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'site_count', '_depth',
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'site_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
@@ -113,6 +114,14 @@ class SiteSerializer(PrimaryModelSerializer):
|
||||
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneSerializerField(required=False)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
@@ -123,7 +132,7 @@ class SiteSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
@@ -138,26 +147,27 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
site = NestedSiteSerializer()
|
||||
parent = NestedLocationSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
]
|
||||
|
||||
|
||||
class RackRoleSerializer(OrganizationalModelSerializer):
|
||||
class RackRoleSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'rack_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'rack_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -169,6 +179,8 @@ class RackSerializer(PrimaryModelSerializer):
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label='Facility ID',
|
||||
default=None)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@@ -181,23 +193,6 @@ class RackSerializer(PrimaryModelSerializer):
|
||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
validators = [
|
||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('facility_id', None):
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RackUnitSerializer(serializers.Serializer):
|
||||
@@ -224,7 +219,7 @@ class RackReservationSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = [
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
@@ -243,10 +238,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
default=RackElevationDetailRenderChoices.RENDER_JSON
|
||||
)
|
||||
unit_width = serializers.IntegerField(
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH')
|
||||
)
|
||||
unit_height = serializers.IntegerField(
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT')
|
||||
)
|
||||
legend_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
@@ -269,7 +264,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
# Device types
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(OrganizationalModelSerializer):
|
||||
class ManufacturerSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = serializers.IntegerField(read_only=True)
|
||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||
@@ -278,7 +273,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
]
|
||||
|
||||
@@ -287,13 +282,14 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'subdevice_role', 'airflow', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count',
|
||||
]
|
||||
|
||||
@@ -356,7 +352,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -425,7 +422,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(OrganizationalModelSerializer):
|
||||
class DeviceRoleSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
@@ -433,12 +430,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
class PlatformSerializer(OrganizationalModelSerializer):
|
||||
class PlatformSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@@ -448,7 +445,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -456,41 +453,31 @@ class DeviceSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
device_role = NestedDeviceRoleSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
||||
rack = NestedRackSerializer(required=False, allow_null=True, default=None)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default='')
|
||||
position = serializers.IntegerField(allow_null=True, label='Position (U)', min_value=1, default=None)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
parent_device = serializers.SerializerMethodField()
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
|
||||
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
|
||||
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
|
||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
validators = []
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('rack') and data.get('position') and data.get('face'):
|
||||
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
|
||||
validator(data, self)
|
||||
|
||||
# Enforce model validation
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
|
||||
def get_parent_device(self, obj):
|
||||
@@ -528,7 +515,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -538,7 +525,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerial
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -547,12 +534,12 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerial
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -562,7 +549,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -571,12 +558,12 @@ class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -585,7 +572,8 @@ class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -600,12 +588,12 @@ class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@@ -619,18 +607,21 @@ class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -639,16 +630,25 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
||||
many=True
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
||||
wireless_lans = SerializedPKRelatedField(
|
||||
queryset=WirelessLAN.objects.all(),
|
||||
serializer=NestedWirelessLANSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
|
||||
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu',
|
||||
'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link',
|
||||
'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint', 'connected_endpoint_type',
|
||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||
'_occupied',
|
||||
'count_fhrp_groups', '_occupied',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -665,7 +665,7 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@@ -675,7 +675,7 @@ class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
@@ -691,7 +691,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'label']
|
||||
|
||||
|
||||
class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@@ -702,7 +702,7 @@ class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields',
|
||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
@@ -727,7 +727,6 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
@@ -754,15 +753,16 @@ class CableSerializer(PrimaryModelSerializer):
|
||||
)
|
||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'custom_fields',
|
||||
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def _get_termination(self, obj, side):
|
||||
@@ -856,7 +856,10 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -875,10 +878,13 @@ class PowerPanelSerializer(PrimaryModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
@@ -908,7 +914,7 @@ class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseForbidden, HttpResponse
|
||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
@@ -17,12 +16,13 @@ from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.exceptions import ServiceUnavailable
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.config import get_config
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related, decode_dict
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
@@ -110,7 +110,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.RegionSerializer
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
@@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
@@ -137,7 +137,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class SiteViewSet(CustomFieldModelViewSet):
|
||||
queryset = Site.objects.prefetch_related(
|
||||
'region', 'tenant', 'tags'
|
||||
'region', 'tenant', 'asns', 'tags'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'site'),
|
||||
rack_count=count_related(Rack, 'site'),
|
||||
@@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
).prefetch_related('site', 'tags')
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
@@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
queryset = RackRole.objects.annotate(
|
||||
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
@@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
@@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
@@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class PlatformViewSet(CustomFieldModelViewSet):
|
||||
queryset = Platform.objects.annotate(
|
||||
queryset = Platform.objects.prefetch_related('tags').annotate(
|
||||
device_count=count_related(Device, 'platform'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||
)
|
||||
@@ -457,9 +457,12 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
username = settings.NAPALM_USERNAME
|
||||
password = settings.NAPALM_PASSWORD
|
||||
optional_args = settings.NAPALM_ARGS.copy()
|
||||
|
||||
config = get_config()
|
||||
username = config.NAPALM_USERNAME
|
||||
password = config.NAPALM_PASSWORD
|
||||
timeout = config.NAPALM_TIMEOUT
|
||||
optional_args = config.NAPALM_ARGS.copy()
|
||||
if device.platform.napalm_args is not None:
|
||||
optional_args.update(device.platform.napalm_args)
|
||||
|
||||
@@ -481,7 +484,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
hostname=host,
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=settings.NAPALM_TIMEOUT,
|
||||
timeout=timeout,
|
||||
optional_args=optional_args
|
||||
)
|
||||
try:
|
||||
@@ -498,7 +501,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||
continue
|
||||
try:
|
||||
response[method] = decode_dict(getattr(d, method)())
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
@@ -513,7 +516,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filtersets.ConsolePortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
@@ -521,7 +524,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
'device', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||
@@ -529,14 +532,14 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
|
||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filtersets.PowerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags')
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filterset_class = filtersets.PowerOutletFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
@@ -544,7 +547,8 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
'device', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
@@ -625,7 +629,7 @@ class PowerPanelViewSet(ModelViewSet):
|
||||
|
||||
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
||||
queryset = PowerFeed.objects.prefetch_related(
|
||||
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerFeedSerializer
|
||||
filterset_class = filtersets.PowerFeedFilterSet
|
||||
@@ -675,15 +679,25 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
if not peer_device_name or not peer_interface_name:
|
||||
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
# Determine local endpoint from peer interface's connection
|
||||
peer_device = get_object_or_404(
|
||||
Device.objects.restrict(request.user, 'view'),
|
||||
name=peer_device_name
|
||||
)
|
||||
peer_interface = get_object_or_404(
|
||||
Interface.objects.all(),
|
||||
device__name=peer_device_name,
|
||||
Interface.objects.restrict(request.user, 'view'),
|
||||
device=peer_device,
|
||||
name=peer_interface_name
|
||||
)
|
||||
local_interface = peer_interface.connected_endpoint
|
||||
endpoint = peer_interface.connected_endpoint
|
||||
|
||||
if local_interface is None:
|
||||
return Response()
|
||||
# If an Interface, return the parent device
|
||||
if type(endpoint) is Interface:
|
||||
device = get_object_or_404(
|
||||
Device.objects.restrict(request.user, 'view'),
|
||||
pk=endpoint.device_id
|
||||
)
|
||||
return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
|
||||
|
||||
return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)
|
||||
# Connected endpoint is none or not an Interface
|
||||
raise Http404
|
||||
|
||||
@@ -174,6 +174,25 @@ class DeviceStatusChoices(ChoiceSet):
|
||||
}
|
||||
|
||||
|
||||
class DeviceAirflowChoices(ChoiceSet):
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
|
||||
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
|
||||
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, 'Front to rear'),
|
||||
(AIRFLOW_REAR_TO_FRONT, 'Rear to front'),
|
||||
(AIRFLOW_LEFT_TO_RIGHT, 'Left to right'),
|
||||
(AIRFLOW_RIGHT_TO_LEFT, 'Right to left'),
|
||||
(AIRFLOW_SIDE_TO_REAR, 'Side to rear'),
|
||||
(AIRFLOW_PASSIVE, 'Passive'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ConsolePorts
|
||||
#
|
||||
@@ -185,6 +204,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_RJ11 = 'rj-11'
|
||||
TYPE_RJ12 = 'rj-12'
|
||||
TYPE_RJ45 = 'rj-45'
|
||||
TYPE_MINI_DIN_8 = 'mini-din-8'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
@@ -192,6 +212,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
@@ -201,6 +222,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_RJ11, 'RJ-11'),
|
||||
(TYPE_RJ12, 'RJ-12'),
|
||||
(TYPE_RJ45, 'RJ-45'),
|
||||
(TYPE_MINI_DIN_8, 'Mini-DIN 8'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
@@ -210,6 +232,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_OTHER, 'Other'),
|
||||
@@ -308,6 +331,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560P = 'nema-l15-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
TYPE_NEMA_L2230P = 'nema-l22-30p'
|
||||
# California style
|
||||
TYPE_CS6361C = 'cs6361c'
|
||||
TYPE_CS6365C = 'cs6365c'
|
||||
@@ -337,6 +361,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_USB_3_B = 'usb-3-b'
|
||||
TYPE_USB_3_MICROB = 'usb-3-micro-b'
|
||||
# Direct current (DC)
|
||||
@@ -412,6 +437,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6361C, 'CS6361C'),
|
||||
@@ -423,7 +449,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
)),
|
||||
('International/ITA', (
|
||||
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/6)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/4)'),
|
||||
(TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
@@ -444,6 +470,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
(TYPE_USB_3_B, 'USB 3.0 Type B'),
|
||||
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
|
||||
)),
|
||||
@@ -527,6 +554,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_L1560R = 'nema-l15-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
TYPE_NEMA_L2230R = 'nema-l22-30r'
|
||||
# California style
|
||||
TYPE_CS6360C = 'CS6360C'
|
||||
TYPE_CS6364C = 'CS6364C'
|
||||
@@ -546,6 +574,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_ITA_M = 'ita-m'
|
||||
TYPE_ITA_N = 'ita-n'
|
||||
TYPE_ITA_O = 'ita-o'
|
||||
TYPE_ITA_MULTISTANDARD = 'ita-multistandard'
|
||||
# USB
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_MICROB = 'usb-micro-b'
|
||||
@@ -624,6 +653,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
|
||||
)),
|
||||
('California Style', (
|
||||
(TYPE_CS6360C, 'CS6360C'),
|
||||
@@ -634,8 +664,8 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_CS8464C, 'CS8464C'),
|
||||
)),
|
||||
('ITA/International', (
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE7/3)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/3)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
(TYPE_ITA_H, 'ITA Type H'),
|
||||
(TYPE_ITA_I, 'ITA Type I'),
|
||||
@@ -645,6 +675,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_ITA_M, 'ITA Type M (BS 546)'),
|
||||
(TYPE_ITA_N, 'ITA Type N'),
|
||||
(TYPE_ITA_O, 'ITA Type O'),
|
||||
(TYPE_ITA_MULTISTANDARD, 'ITA Multistandard'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
@@ -681,10 +712,23 @@ class PowerOutletFeedLegChoices(ChoiceSet):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceKindChoices(ChoiceSet):
|
||||
KIND_PHYSICAL = 'physical'
|
||||
KIND_VIRTUAL = 'virtual'
|
||||
KIND_WIRELESS = 'wireless'
|
||||
|
||||
CHOICES = (
|
||||
(KIND_PHYSICAL, 'Physical'),
|
||||
(KIND_VIRTUAL, 'Virtual'),
|
||||
(KIND_WIRELESS, 'Wireless'),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Virtual
|
||||
TYPE_VIRTUAL = 'virtual'
|
||||
TYPE_BRIDGE = 'bridge'
|
||||
TYPE_LAG = 'lag'
|
||||
|
||||
# Ethernet
|
||||
@@ -721,6 +765,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
@@ -744,7 +789,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
|
||||
|
||||
# InfiniBand
|
||||
TYPE_INFINIBAND_SDR = 'infiniband-sdr'
|
||||
@@ -771,6 +816,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||
TYPE_STACKWISE80 = 'cisco-stackwise-80'
|
||||
TYPE_STACKWISE160 = 'cisco-stackwise-160'
|
||||
TYPE_STACKWISE320 = 'cisco-stackwise-320'
|
||||
TYPE_STACKWISE480 = 'cisco-stackwise-480'
|
||||
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||
@@ -785,6 +834,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
'Virtual interfaces',
|
||||
(
|
||||
(TYPE_VIRTUAL, 'Virtual'),
|
||||
(TYPE_BRIDGE, 'Bridge'),
|
||||
(TYPE_LAG, 'Link Aggregation Group (LAG)'),
|
||||
),
|
||||
),
|
||||
@@ -832,6 +882,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -903,6 +954,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||
(TYPE_STACKWISE80, 'Cisco StackWise-80'),
|
||||
(TYPE_STACKWISE160, 'Cisco StackWise-160'),
|
||||
(TYPE_STACKWISE320, 'Cisco StackWise-320'),
|
||||
(TYPE_STACKWISE480, 'Cisco StackWise-480'),
|
||||
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||
@@ -1026,7 +1081,7 @@ class PortTypeChoices(ChoiceSet):
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
# Cables/links
|
||||
#
|
||||
|
||||
class CableTypeChoices(ChoiceSet):
|
||||
@@ -1090,7 +1145,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class CableStatusChoices(ChoiceSet):
|
||||
class LinkStatusChoices(ChoiceSet):
|
||||
|
||||
STATUS_CONNECTED = 'connected'
|
||||
STATUS_PLANNED = 'planned'
|
||||
|
||||
@@ -34,6 +34,7 @@ INTERFACE_MTU_MAX = 65536
|
||||
VIRTUAL_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
InterfaceTypeChoices.TYPE_LAG,
|
||||
InterfaceTypeChoices.TYPE_BRIDGE,
|
||||
]
|
||||
|
||||
WIRELESS_IFACE_TYPES = [
|
||||
@@ -42,6 +43,7 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211N,
|
||||
InterfaceTypeChoices.TYPE_80211AC,
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
InterfaceTypeChoices.TYPE_80211AX,
|
||||
]
|
||||
|
||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
@@ -2,11 +2,30 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
||||
|
||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||
from .lookups import PathContains
|
||||
|
||||
__all__ = (
|
||||
'ASNField',
|
||||
'MACAddressField',
|
||||
'PathField',
|
||||
'WWNField',
|
||||
)
|
||||
|
||||
|
||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
|
||||
|
||||
class eui64_unix_expanded_uppercase(eui64_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
|
||||
|
||||
#
|
||||
# Fields
|
||||
#
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
description = "32-bit ASN field"
|
||||
@@ -24,10 +43,6 @@ class ASNField(models.BigIntegerField):
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
|
||||
|
||||
class MACAddressField(models.Field):
|
||||
description = "PostgreSQL MAC Address field"
|
||||
|
||||
@@ -42,8 +57,8 @@ class MACAddressField(models.Field):
|
||||
return value
|
||||
try:
|
||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||
except AddrFormatError as e:
|
||||
raise ValidationError("Invalid MAC address format: {}".format(value))
|
||||
except AddrFormatError:
|
||||
raise ValidationError(f"Invalid MAC address format: {value}")
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'macaddr'
|
||||
@@ -54,6 +69,32 @@ class MACAddressField(models.Field):
|
||||
return str(self.to_python(value))
|
||||
|
||||
|
||||
class WWNField(models.Field):
|
||||
description = "World Wide Name field"
|
||||
|
||||
def python_type(self):
|
||||
return EUI
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return self.to_python(value)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
||||
except AddrFormatError:
|
||||
raise ValidationError(f"Invalid WWN format: {value}")
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'macaddr8'
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return str(self.to_python(value))
|
||||
|
||||
|
||||
class PathField(ArrayField):
|
||||
"""
|
||||
An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
||||
|
||||
from extras.filters import TagFilter
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||
)
|
||||
@@ -10,14 +11,15 @@ from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CableFilterSet',
|
||||
'CableTerminationFilterSet',
|
||||
@@ -71,6 +73,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Parent region (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
@@ -88,6 +91,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Parent site group (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
@@ -127,6 +131,11 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
asn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns',
|
||||
queryset=ASN.objects.all(),
|
||||
label='AS (ID)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
@@ -152,12 +161,13 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
qs_filter |= Q(asns__asn=int(value.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class LocationFilterSet(OrganizationalModelFilterSet):
|
||||
class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -207,6 +217,7 @@ class LocationFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Location (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
@@ -222,6 +233,7 @@ class LocationFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
@@ -387,6 +399,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
|
||||
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -441,7 +454,7 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -480,12 +493,21 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
label='Device type (ID)',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
|
||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
@@ -560,6 +582,7 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
|
||||
|
||||
|
||||
class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@@ -578,6 +601,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@@ -694,7 +718,7 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
||||
field_name='interfaces__mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
serial = django_filters.CharFilter(
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
@@ -742,7 +766,7 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', 'asset_tag', 'face', 'position', 'vc_position', 'vc_priority']
|
||||
fields = ['id', 'name', 'asset_tag', 'face', 'position', 'airflow', 'vc_position', 'vc_priority']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -852,6 +876,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual Chassis (ID)'
|
||||
)
|
||||
virtual_chassis = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis__name',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Virtual Chassis',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -958,12 +993,18 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
queryset=Interface.objects.all(),
|
||||
label='Parent interface (ID)',
|
||||
)
|
||||
bridge_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='bridge',
|
||||
queryset=Interface.objects.all(),
|
||||
label='Bridged interface (ID)',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
label='LAG interface (ID)',
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter()
|
||||
wwn = MultiValueWWNFilter()
|
||||
tag = TagFilter()
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
@@ -977,10 +1018,19 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
choices=InterfaceTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
rf_role = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessRoleChoices
|
||||
)
|
||||
rf_channel = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessChannelChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
|
||||
fields = [
|
||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
||||
]
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
@@ -1179,16 +1229,20 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
class CableFilterSet(PrimaryModelFilterSet):
|
||||
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
termination_a_type = ContentTypeFilter()
|
||||
termination_a_id = MultiValueNumberFilter()
|
||||
termination_b_type = ContentTypeFilter()
|
||||
termination_b_id = MultiValueNumberFilter()
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CableTypeChoices
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CableStatusChoices
|
||||
choices=LinkStatusChoices
|
||||
)
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=ColorChoices
|
||||
@@ -1204,7 +1258,7 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__rack_id'
|
||||
)
|
||||
rack = MultiValueNumberFilter(
|
||||
rack = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__rack__name'
|
||||
)
|
||||
@@ -1212,23 +1266,15 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__site_id'
|
||||
)
|
||||
site = MultiValueNumberFilter(
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
tenant_id = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='device__tenant_id'
|
||||
)
|
||||
tenant = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='device__tenant__slug'
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit']
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1243,73 +1289,6 @@ class CableFilterSet(PrimaryModelFilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -1441,3 +1420,65 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
#
|
||||
# Connection filter sets
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site_id'
|
||||
)
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device_id'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
def filter_connections(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cable__label__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .fields import *
|
||||
from .models import *
|
||||
from .filtersets import *
|
||||
from .object_create import *
|
||||
|
||||
@@ -3,7 +3,7 @@ from django import forms
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, form_from_model
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, form_from_model
|
||||
from .object_create import ComponentForm
|
||||
|
||||
__all__ = (
|
||||
@@ -23,7 +23,7 @@ __all__ = (
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
@@ -6,12 +7,12 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||
from ipam.models import VLAN
|
||||
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
|
||||
from ipam.models import VLAN, ASN
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@@ -51,7 +52,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -69,7 +70,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -87,7 +88,7 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['parent', 'description']
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -116,6 +117,11 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@@ -128,11 +134,11 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'region', 'group', 'tenant', 'asn', 'description', 'time_zone',
|
||||
'region', 'group', 'tenant', 'asn', 'asns', 'description', 'time_zone',
|
||||
]
|
||||
|
||||
|
||||
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -148,16 +154,20 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['parent', 'description']
|
||||
nullable_fields = ['parent', 'tenant', 'description']
|
||||
|
||||
|
||||
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -174,7 +184,7 @@ class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -274,7 +284,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
||||
]
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -299,7 +309,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -313,7 +323,7 @@ class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -331,12 +341,17 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label='Is full depth'
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
nullable_fields = ['airflow']
|
||||
|
||||
|
||||
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -358,7 +373,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['color', 'description']
|
||||
|
||||
|
||||
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -381,7 +396,7 @@ class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
||||
nullable_fields = ['manufacturer', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -425,6 +440,11 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
serial = forms.CharField(
|
||||
max_length=50,
|
||||
required=False,
|
||||
@@ -433,11 +453,11 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'tenant', 'platform', 'serial',
|
||||
'tenant', 'platform', 'serial', 'airflow',
|
||||
]
|
||||
|
||||
|
||||
class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Cable.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -449,11 +469,15 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
||||
widget=StaticSelect()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CableStatusChoices),
|
||||
choices=add_blank_choice(LinkStatusChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
initial=''
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
label = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@@ -474,7 +498,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'type', 'status', 'label', 'color', 'length',
|
||||
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -489,7 +513,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
||||
})
|
||||
|
||||
|
||||
class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -503,7 +527,7 @@ class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM
|
||||
nullable_fields = ['domain']
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -542,7 +566,7 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
||||
nullable_fields = ['location']
|
||||
|
||||
|
||||
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
@@ -607,7 +631,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
||||
# Device component templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ConsolePortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsolePortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -626,7 +650,7 @@ class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConsoleServerPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -648,7 +672,7 @@ class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class PowerPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -680,7 +704,7 @@ class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class PowerOutletTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerOutletTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -728,7 +752,7 @@ class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
self.fields['power_port'].widget.attrs['disabled'] = True
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -755,7 +779,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('label', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class FrontPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -780,7 +804,7 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class RearPortTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -805,7 +829,7 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class DeviceBayTemplateBulkEditForm(BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=DeviceBayTemplate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -828,7 +852,6 @@ class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class ConsolePortBulkEditForm(
|
||||
form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -847,7 +870,6 @@ class ConsolePortBulkEditForm(
|
||||
|
||||
class ConsoleServerPortBulkEditForm(
|
||||
form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -866,7 +888,6 @@ class ConsoleServerPortBulkEditForm(
|
||||
|
||||
class PowerPortBulkEditForm(
|
||||
form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -885,7 +906,6 @@ class PowerPortBulkEditForm(
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -921,9 +941,9 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode',
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
]),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -945,6 +965,10 @@ class InterfaceBulkEditForm(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@@ -972,7 +996,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'label', 'parent', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
|
||||
'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -980,8 +1005,9 @@ class InterfaceBulkEditForm(
|
||||
if 'device' in self.initial:
|
||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||
|
||||
# Restrict parent/LAG interface assignment by device
|
||||
# Restrict parent/bridge/LAG interface assignment by device
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
@@ -1009,14 +1035,22 @@ class InterfaceBulkEditForm(
|
||||
|
||||
self.fields['parent'].choices = ()
|
||||
self.fields['parent'].widget.attrs['disabled'] = True
|
||||
self.fields['bridge'].choices = ()
|
||||
self.fields['bridge'].widget.attrs['disabled'] = True
|
||||
self.fields['lag'].choices = ()
|
||||
self.fields['lag'].widget.attrs['disabled'] = True
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if not self.cleaned_data['mode']:
|
||||
if self.cleaned_data['untagged_vlan']:
|
||||
raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"})
|
||||
elif self.cleaned_data['tagged_vlans']:
|
||||
raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"})
|
||||
|
||||
# Untagged interfaces cannot be assigned tagged VLANs
|
||||
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
||||
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
||||
raise forms.ValidationError({
|
||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||
})
|
||||
@@ -1028,7 +1062,6 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class FrontPortBulkEditForm(
|
||||
form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1043,7 +1076,6 @@ class FrontPortBulkEditForm(
|
||||
|
||||
class RearPortBulkEditForm(
|
||||
form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1058,7 +1090,6 @@ class RearPortBulkEditForm(
|
||||
|
||||
class DeviceBayBulkEditForm(
|
||||
form_from_model(DeviceBay, ['label', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
@@ -1073,7 +1104,6 @@ class DeviceBayBulkEditForm(
|
||||
|
||||
class InventoryItemBulkEditForm(
|
||||
form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
CustomFieldModelBulkEditForm
|
||||
):
|
||||
|
||||
@@ -11,6 +11,7 @@ from extras.forms import CustomFieldModelCSVForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
||||
from virtualization.models import Cluster
|
||||
from wireless.choices import WirelessRoleChoices
|
||||
|
||||
__all__ = (
|
||||
'CableCSVForm',
|
||||
@@ -94,7 +95,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments',
|
||||
)
|
||||
@@ -120,10 +121,16 @@ class LocationCSVForm(CustomFieldModelCSVForm):
|
||||
'invalid_choice': 'Location not found.',
|
||||
}
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ('site', 'parent', 'name', 'slug', 'description')
|
||||
fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description')
|
||||
|
||||
|
||||
class RackRoleCSVForm(CustomFieldModelCSVForm):
|
||||
@@ -363,12 +370,17 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
required=False,
|
||||
help_text='Mounted rack face'
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
choices=DeviceAirflowChoices,
|
||||
required=False,
|
||||
help_text='Airflow direction'
|
||||
)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster',
|
||||
'comments',
|
||||
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||
'cluster', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@@ -558,6 +570,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
to_field_name='name',
|
||||
help_text='Parent interface'
|
||||
)
|
||||
bridge = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Bridged interface'
|
||||
)
|
||||
lag = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@@ -573,42 +591,20 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
required=False,
|
||||
help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
|
||||
)
|
||||
rf_role = CSVChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
help_text='Wireless role (AP/station)'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'description', 'mode',
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
|
||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
|
||||
device = None
|
||||
if self.is_bound and 'device' in self.data:
|
||||
try:
|
||||
device = self.fields['device'].to_python(self.data['device'])
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
if device and device.virtual_chassis:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(
|
||||
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis)
|
||||
)
|
||||
elif device:
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=device,
|
||||
type=InterfaceTypeChoices.TYPE_LAG
|
||||
)
|
||||
self.fields['parent'].queryset = Interface.objects.filter(device=device)
|
||||
else:
|
||||
self.fields['lag'].queryset = Interface.objects.none()
|
||||
self.fields['parent'].queryset = Interface.objects.none()
|
||||
|
||||
def clean_enabled(self):
|
||||
# Make sure enabled is True when it's not included in the uploaded data
|
||||
if 'enabled' not in self.data:
|
||||
@@ -801,7 +797,7 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
# Cable attributes
|
||||
status = CSVChoiceField(
|
||||
choices=CableStatusChoices,
|
||||
choices=LinkStatusChoices,
|
||||
required=False,
|
||||
help_text='Connection status'
|
||||
)
|
||||
@@ -810,6 +806,12 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
required=False,
|
||||
help_text='Physical medium classification'
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
)
|
||||
length_unit = CSVChoiceField(
|
||||
choices=CableLengthUnitChoices,
|
||||
required=False,
|
||||
@@ -820,7 +822,7 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
||||
model = Cable
|
||||
fields = [
|
||||
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
|
||||
'status', 'label', 'color', 'length', 'length_unit',
|
||||
'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
help_texts = {
|
||||
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
|
||||
|
||||
@@ -2,7 +2,8 @@ from circuits.models import Circuit, CircuitTermination, Provider
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||
|
||||
__all__ = (
|
||||
'ConnectCableToCircuitTerminationForm',
|
||||
@@ -17,7 +18,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm):
|
||||
"""
|
||||
Base form for connecting a Cable to a Device component
|
||||
"""
|
||||
@@ -26,7 +27,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -37,7 +38,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
@@ -77,8 +78,9 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_rack',
|
||||
'termination_b_device', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect,
|
||||
@@ -169,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
||||
)
|
||||
|
||||
|
||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm):
|
||||
termination_b_provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
@@ -180,7 +182,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -191,7 +193,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_circuit = DynamicModelChoiceField(
|
||||
@@ -215,11 +217,11 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
|
||||
'termination_b_circuit', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
def clean_termination_b_id(self):
|
||||
@@ -227,13 +229,13 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||
|
||||
|
||||
class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm):
|
||||
termination_b_region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
required=False
|
||||
)
|
||||
termination_b_site_group = DynamicModelChoiceField(
|
||||
termination_b_sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
required=False
|
||||
@@ -244,7 +246,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$termination_b_region',
|
||||
'group_id': '$termination_b_site_group',
|
||||
'group_id': '$termination_b_sitegroup',
|
||||
}
|
||||
)
|
||||
termination_b_location = DynamicModelChoiceField(
|
||||
@@ -277,10 +279,10 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_location',
|
||||
'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label',
|
||||
'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from django import forms
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
__all__ = (
|
||||
'MACAddressField',
|
||||
)
|
||||
|
||||
|
||||
class MACAddressField(forms.Field):
|
||||
widget = forms.CharField
|
||||
default_error_messages = {
|
||||
'invalid': 'MAC address must be in EUI-48 format',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
|
||||
# Validate MAC address format
|
||||
try:
|
||||
value = EUI(value.strip())
|
||||
except AddrFormatError:
|
||||
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
||||
|
||||
return value
|
||||
@@ -6,12 +6,13 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||
from ipam.models import ASN
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
|
||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from wireless.choices import *
|
||||
|
||||
__all__ = (
|
||||
'CableFilterForm',
|
||||
@@ -46,15 +47,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
field_order = [
|
||||
'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id',
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
class DeviceComponentFilterForm(CustomFieldModelFilterForm):
|
||||
name = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@@ -64,14 +57,12 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -80,8 +71,7 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -89,8 +79,12 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual Chassis')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -98,63 +92,40 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
'virtual_chassis_id': '$virtual_chassis_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class RegionFilterForm(CustomFieldModelFilterForm):
|
||||
model = Region
|
||||
field_groups = [
|
||||
['q'],
|
||||
['parent_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent region')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class SiteGroupFilterForm(CustomFieldModelFilterForm):
|
||||
model = SiteGroup
|
||||
field_groups = [
|
||||
['q'],
|
||||
['parent_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent group')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Site
|
||||
field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['status', 'region_id', 'group_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
['asn_id']
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=SiteStatusChoices,
|
||||
required=False,
|
||||
@@ -163,36 +134,37 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
label=_('ASNs')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Location
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'parent_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -201,8 +173,7 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
parent_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -211,26 +182,18 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Parent'),
|
||||
fetch_trigger='open'
|
||||
label=_('Parent')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class RackRoleFilterForm(CustomFieldModelFilterForm):
|
||||
model = RackRole
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Rack
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
@@ -238,16 +201,10 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
['type', 'width', 'serial', 'asset_tag'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -255,8 +212,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -265,8 +221,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
@@ -287,8 +242,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
label=_('Role')
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@@ -300,10 +254,6 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
|
||||
|
||||
class RackElevationFilterForm(RackFilterForm):
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id',
|
||||
'tenant_id',
|
||||
]
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label=_('Rack'),
|
||||
@@ -311,30 +261,22 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = RackReservation
|
||||
field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['user_id'],
|
||||
['region_id', 'site_id', 'location_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -342,15 +284,13 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.prefetch_related('site'),
|
||||
required=False,
|
||||
label=_('Location'),
|
||||
null_option='None',
|
||||
fetch_trigger='open'
|
||||
null_option='None'
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
@@ -358,47 +298,38 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
|
||||
label=_('User'),
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ManufacturerFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class ManufacturerFilterForm(CustomFieldModelFilterForm):
|
||||
model = Manufacturer
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class DeviceTypeFilterForm(CustomFieldModelFilterForm):
|
||||
model = DeviceType
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['manufacturer_id', 'subdevice_role'],
|
||||
['manufacturer_id', 'subdevice_role', 'airflow'],
|
||||
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
subdevice_role = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(SubdeviceRoleChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console ports',
|
||||
@@ -444,43 +375,27 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class DeviceRoleFilterForm(CustomFieldModelFilterForm):
|
||||
model = DeviceRole
|
||||
field_groups = [
|
||||
['q'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PlatformFilterForm(CustomFieldModelFilterForm):
|
||||
model = Platform
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Device
|
||||
field_order = [
|
||||
'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id',
|
||||
'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
|
||||
]
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'],
|
||||
['status', 'role_id', 'serial', 'asset_tag', 'mac_address'],
|
||||
['status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address'],
|
||||
['manufacturer_id', 'device_type_id', 'platform_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
[
|
||||
@@ -488,22 +403,15 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
|
||||
],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -512,8 +420,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -522,8 +429,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -533,20 +439,17 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_('Rack'),
|
||||
fetch_trigger='open'
|
||||
label=_('Rack')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
label=_('Role')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
@@ -554,21 +457,24 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer_id'
|
||||
},
|
||||
label=_('Model'),
|
||||
fetch_trigger='open'
|
||||
label=_('Model')
|
||||
)
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Platform'),
|
||||
fetch_trigger='open'
|
||||
label=_('Platform')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@@ -638,30 +544,22 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = VirtualChassis
|
||||
field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -670,30 +568,23 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||
model = Cable
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['site_id', 'rack_id', 'device_id'],
|
||||
['type', 'status', 'color'],
|
||||
['tenant_id'],
|
||||
['type', 'status', 'color', 'length', 'length_unit'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -701,14 +592,7 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
tenant_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
label=_('Tenant'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -717,21 +601,7 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(CableStatusChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
}
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -741,34 +611,46 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'tenant_id': '$tenant_id',
|
||||
'rack_id': '$rack_id',
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=add_blank_choice(CableTypeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(LinkStatusChoices),
|
||||
widget=StaticSelect()
|
||||
)
|
||||
color = ColorField(
|
||||
required=False
|
||||
)
|
||||
length = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
length_unit = forms.ChoiceField(
|
||||
choices=add_blank_choice(CableLengthUnitChoices),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PowerPanelFilterForm(CustomFieldModelFilterForm):
|
||||
model = PowerPanel
|
||||
field_groups = (
|
||||
('q', 'tag'),
|
||||
('region_id', 'site_group_id', 'site_id', 'location_id')
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -777,8 +659,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
@@ -787,13 +668,12 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
label=_('Location')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
class PowerFeedFilterForm(CustomFieldModelFilterForm):
|
||||
model = PowerFeed
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
@@ -801,22 +681,15 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
['power_panel_id', 'rack_id'],
|
||||
['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -824,8 +697,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
power_panel_id = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
@@ -834,8 +706,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Power panel'),
|
||||
fetch_trigger='open'
|
||||
label=_('Power panel')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -844,8 +715,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Rack'),
|
||||
fetch_trigger='open'
|
||||
label=_('Rack')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
@@ -888,7 +758,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -908,7 +778,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -928,7 +798,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -943,7 +813,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -957,9 +827,15 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||
['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
kind = forms.MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False,
|
||||
@@ -981,6 +857,36 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
required=False,
|
||||
label='MAC address'
|
||||
)
|
||||
wwn = forms.CharField(
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
rf_role = forms.MultipleChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless role'
|
||||
)
|
||||
rf_channel = forms.MultipleChoiceField(
|
||||
choices=WirelessChannelChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple(),
|
||||
label='Wireless channel'
|
||||
)
|
||||
rf_channel_frequency = forms.IntegerField(
|
||||
required=False,
|
||||
label='Channel frequency (MHz)'
|
||||
)
|
||||
rf_channel_width = forms.IntegerField(
|
||||
required=False,
|
||||
label='Channel width (MHz)'
|
||||
)
|
||||
tx_power = forms.IntegerField(
|
||||
required=False,
|
||||
label='Transmit power (dBm)',
|
||||
min_value=0,
|
||||
max_value=127
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@@ -988,7 +894,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1007,7 +913,7 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
@@ -1025,7 +931,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1035,13 +941,12 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer'),
|
||||
fetch_trigger='open'
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
serial = forms.CharField(
|
||||
required=False
|
||||
@@ -1062,12 +967,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1075,8 +979,7 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1084,17 +987,15 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class PowerConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1102,8 +1003,7 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1111,17 +1011,15 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
class InterfaceConnectionFilterForm(FilterForm):
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region'),
|
||||
fetch_trigger='open'
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -1129,8 +1027,7 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site'),
|
||||
fetch_trigger='open'
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
@@ -1138,6 +1035,5 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
label=_('Device')
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from timezone_field import TimeZoneFormField
|
||||
@@ -8,7 +9,7 @@ from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
||||
@@ -16,6 +17,7 @@ from utilities.forms import (
|
||||
SlugField, StaticSelect,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
@@ -38,6 +40,7 @@ __all__ = (
|
||||
'LocationForm',
|
||||
'ManufacturerForm',
|
||||
'PlatformForm',
|
||||
'PopulateDeviceBayForm',
|
||||
'PowerFeedForm',
|
||||
'PowerOutletForm',
|
||||
'PowerOutletTemplateForm',
|
||||
@@ -52,6 +55,7 @@ __all__ = (
|
||||
'RegionForm',
|
||||
'SiteForm',
|
||||
'SiteGroupForm',
|
||||
'VCMemberSelectForm',
|
||||
'VirtualChassisForm',
|
||||
)
|
||||
|
||||
@@ -62,35 +66,43 @@ Tagged (All): Implies all VLANs are available (w/optional untagged VLAN)
|
||||
"""
|
||||
|
||||
|
||||
class RegionForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RegionForm(CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = (
|
||||
'parent', 'name', 'slug', 'description',
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class SiteGroupForm(CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
'parent', 'name', 'slug', 'description',
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class SiteForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
@@ -99,6 +111,11 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
time_zone = TimeZoneFormField(
|
||||
choices=add_blank_choice(TimeZoneFormField().choices),
|
||||
@@ -114,13 +131,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'asns',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Site', (
|
||||
'name', 'slug', 'status', 'region', 'group', 'facility', 'asn', 'time_zone', 'description', 'tags',
|
||||
'name', 'slug', 'status', 'region', 'group', 'facility', 'asn', 'asns', 'time_zone', 'description',
|
||||
'tags',
|
||||
)),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
('Contact Info', (
|
||||
@@ -144,8 +162,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the site",
|
||||
'asn': "BGP autonomous system number. This field is depreciated in favour of the ASN model",
|
||||
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||
'asn': "BGP autonomous system number",
|
||||
'time_zone': "Local time zone",
|
||||
'description': "Short description (will appear in sites list)",
|
||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||
@@ -155,7 +173,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class LocationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class LocationForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -185,25 +203,39 @@ class LocationForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = (
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
)
|
||||
fieldsets = (
|
||||
('Location', (
|
||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
||||
|
||||
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RackRoleForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description',
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class RackForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -263,22 +295,20 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class RackReservationForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
site_group = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -286,24 +316,21 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$site_group',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
},
|
||||
fetch_trigger='open'
|
||||
}
|
||||
)
|
||||
units = NumericArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
@@ -317,8 +344,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
fetch_trigger='open'
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -333,17 +359,21 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ManufacturerForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'name', 'slug', 'description',
|
||||
'name', 'slug', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceTypeForm(CustomFieldModelForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all()
|
||||
)
|
||||
@@ -359,12 +389,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
'front_image', 'rear_image', 'comments', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Device Type', (
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'tags',
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'tags',
|
||||
)),
|
||||
('Chassis', (
|
||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||
)),
|
||||
('Images', ('front_image', 'rear_image')),
|
||||
)
|
||||
@@ -379,17 +412,21 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceRoleForm(CustomFieldModelForm):
|
||||
slug = SlugField()
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'vm_role', 'description',
|
||||
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PlatformForm(CustomFieldModelForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
@@ -397,18 +434,22 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
||||
slug = SlugField(
|
||||
max_length=64
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'napalm_args': SmallTextarea(),
|
||||
}
|
||||
|
||||
|
||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class DeviceForm(TenancyForm, CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -511,8 +552,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
model = Device
|
||||
fields = [
|
||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
|
||||
'location', 'position', 'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group',
|
||||
'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
|
||||
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
|
||||
]
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
@@ -523,6 +564,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
widgets = {
|
||||
'face': StaticSelect(),
|
||||
'status': StaticSelect(),
|
||||
'airflow': StaticSelect(),
|
||||
'primary_ip4': StaticSelect(),
|
||||
'primary_ip6': StaticSelect(),
|
||||
}
|
||||
@@ -589,7 +631,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
||||
|
||||
|
||||
class CableForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class CableForm(TenancyForm, CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -598,7 +640,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect,
|
||||
@@ -612,7 +654,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerPanelForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -656,7 +698,7 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerFeedForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -724,7 +766,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class VirtualChassisForm(CustomFieldModelForm):
|
||||
master = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
@@ -750,7 +792,6 @@ class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
|
||||
|
||||
|
||||
class DeviceVCMembershipForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
@@ -846,7 +887,6 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
@@ -858,7 +898,6 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
@@ -870,7 +909,6 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
@@ -882,7 +920,6 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
@@ -893,7 +930,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit power_port choices to current DeviceType
|
||||
@@ -904,7 +940,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
@@ -917,7 +952,6 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
@@ -929,7 +963,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit rear_port choices to current DeviceType
|
||||
@@ -940,7 +973,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
@@ -953,7 +985,6 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
@@ -968,7 +999,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConsolePortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -984,7 +1015,7 @@ class ConsolePortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class ConsoleServerPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1000,7 +1031,7 @@ class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1017,7 +1048,7 @@ class PowerPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class PowerOutletForm(CustomFieldModelForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
@@ -1046,12 +1077,17 @@ class PowerOutletForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Parent interface'
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface'
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@@ -1060,6 +1096,19 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
'type': 'lag',
|
||||
}
|
||||
)
|
||||
wireless_lan_group = DynamicModelChoiceField(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='Wireless LAN group'
|
||||
)
|
||||
wireless_lans = DynamicModelMultipleChoiceField(
|
||||
queryset=WirelessLAN.objects.all(),
|
||||
required=False,
|
||||
label='Wireless LANs',
|
||||
query_params={
|
||||
'group_id': '$wireless_lan_group',
|
||||
}
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
@@ -1089,19 +1138,24 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
|
||||
'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'mode': StaticSelect(),
|
||||
'rf_role': StaticSelect(),
|
||||
'rf_channel': StaticSelect(),
|
||||
}
|
||||
labels = {
|
||||
'mode': '802.1Q Mode',
|
||||
}
|
||||
help_texts = {
|
||||
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||
'rf_channel_frequency': "Populated by selected channel (if set)",
|
||||
'rf_channel_width': "Populated by selected channel (if set)",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1109,20 +1163,21 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
|
||||
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||
|
||||
# Restrict parent/LAG interface assignment by device/VC
|
||||
# Restrict parent/bridge/LAG interface assignment by device/VC
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
if device.virtual_chassis and device.virtual_chassis.master:
|
||||
# Get available LAG interfaces by VirtualChassis master
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
else:
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
||||
|
||||
class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class FrontPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1150,7 +1205,7 @@ class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
)
|
||||
|
||||
|
||||
class RearPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class RearPortForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1167,7 +1222,7 @@ class RearPortForm(BootstrapMixin, CustomFieldModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class DeviceBayForm(CustomFieldModelForm):
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
@@ -1192,7 +1247,6 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
|
||||
def __init__(self, device_bay, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['installed_device'].queryset = Device.objects.filter(
|
||||
@@ -1204,7 +1258,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
).exclude(pk=device_bay.device.pk)
|
||||
|
||||
|
||||
class InventoryItemForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class InventoryItemForm(CustomFieldModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
ExpandableNameField, StaticSelect,
|
||||
)
|
||||
from wireless.choices import *
|
||||
from .common import InterfaceCommonForm
|
||||
|
||||
__all__ = (
|
||||
@@ -34,7 +35,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ComponentForm(forms.Form):
|
||||
class ComponentForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
a name pattern.
|
||||
@@ -62,7 +63,7 @@ class ComponentForm(forms.Form):
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
class VirtualChassisCreateForm(CustomFieldModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -117,12 +118,18 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
|
||||
raise forms.ValidationError({
|
||||
'initial_position': "A position must be specified for the first VC member."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign VC members
|
||||
if instance.pk:
|
||||
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||
if instance.pk and self.cleaned_data['members']:
|
||||
initial_position = self.cleaned_data.get('initial_position', 1)
|
||||
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
|
||||
member.virtual_chassis = instance
|
||||
member.vc_position = i
|
||||
@@ -135,7 +142,7 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm):
|
||||
class ComponentTemplateCreateForm(ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
|
||||
"""
|
||||
@@ -328,7 +335,7 @@ class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
|
||||
class ComponentCreateForm(CustomFieldsMixin, ComponentForm):
|
||||
"""
|
||||
Base form for the creation of device components (models subclassed from ComponentModel).
|
||||
"""
|
||||
@@ -445,18 +452,30 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
'type': 'lag',
|
||||
}
|
||||
},
|
||||
label='LAG'
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
)
|
||||
wwn = forms.CharField(
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='Management only',
|
||||
@@ -465,19 +484,42 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfaceModeChoices),
|
||||
required=False,
|
||||
widget=StaticSelect()
|
||||
)
|
||||
rf_role = forms.ChoiceField(
|
||||
choices=add_blank_choice(WirelessRoleChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
label='Wireless role'
|
||||
)
|
||||
rf_channel = forms.ChoiceField(
|
||||
choices=add_blank_choice(WirelessChannelChoices),
|
||||
required=False,
|
||||
widget=StaticSelect(),
|
||||
label='Wireless channel'
|
||||
)
|
||||
rf_channel_frequency = forms.DecimalField(
|
||||
required=False,
|
||||
label='Channel frequency (MHz)'
|
||||
)
|
||||
rf_channel_width = forms.DecimalField(
|
||||
required=False,
|
||||
label='Channel width (MHz)'
|
||||
)
|
||||
untagged_vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
label='Untagged VLAN'
|
||||
)
|
||||
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
label='Tagged VLANs'
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
|
||||
'wwn', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user