mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 15:47:46 -06:00
Compare commits
1047 Commits
v1.8.3
...
v2.3-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1033c8677a | ||
|
|
b2c5bcd4f1 | ||
|
|
73c64272d8 | ||
|
|
11fe54753e | ||
|
|
69f921aea9 | ||
|
|
594ef71027 | ||
|
|
d25d8c21f6 | ||
|
|
835d13542f | ||
|
|
7f5a3fffd3 | ||
|
|
1890e710cb | ||
|
|
a9fefbec5c | ||
|
|
b96e3af6c7 | ||
|
|
12e6fe1d50 | ||
|
|
60c03a646c | ||
|
|
59dcbce417 | ||
|
|
df10fa87d3 | ||
|
|
a954406d1f | ||
|
|
e2213f458f | ||
|
|
55adcc1f0c | ||
|
|
d6eaa3d0cc | ||
|
|
25ad58d42c | ||
|
|
b61bccbb67 | ||
|
|
f1da517c84 | ||
|
|
a4019be28c | ||
|
|
36090d9f02 | ||
|
|
6b101d2c49 | ||
|
|
b3243704df | ||
|
|
8bedfcfc64 | ||
|
|
e0aa2c33e9 | ||
|
|
49f268a14c | ||
|
|
2bb0e65aea | ||
|
|
8b6d731cb6 | ||
|
|
1cd629efb3 | ||
|
|
2f7f5425d8 | ||
|
|
215156c333 | ||
|
|
a5d2055c11 | ||
|
|
ffc2c564b8 | ||
|
|
16f222b0ab | ||
|
|
3edf90714a | ||
|
|
4e8fc03c2b | ||
|
|
5037283b62 | ||
|
|
f2c9135b96 | ||
|
|
7378d82bc4 | ||
|
|
ed10a99771 | ||
|
|
4df128d34e | ||
|
|
33d0db5854 | ||
|
|
4a57a554da | ||
|
|
21fe7c57d8 | ||
|
|
3bcc1429dd | ||
|
|
9a6a479452 | ||
|
|
c43487b741 | ||
|
|
6b50755a5a | ||
|
|
53998e0fff | ||
|
|
7341ae087c | ||
|
|
9ea8dca4e3 | ||
|
|
d80e64b2cc | ||
|
|
d7354f4dab | ||
|
|
8d1676db54 | ||
|
|
0714a40509 | ||
|
|
5262156e1a | ||
|
|
7ac27b59c6 | ||
|
|
fc7a43f23e | ||
|
|
d5ecfe7bef | ||
|
|
e58d1ac87e | ||
|
|
bb653e733c | ||
|
|
9c27d18d6c | ||
|
|
e5c13d2d72 | ||
|
|
b27529d927 | ||
|
|
95257114df | ||
|
|
935da0d51f | ||
|
|
78ed85943b | ||
|
|
7d87cc498a | ||
|
|
a523d25c0d | ||
|
|
d84e5d1839 | ||
|
|
063e79451f | ||
|
|
b4a842d9da | ||
|
|
841471104b | ||
|
|
ac71416eb9 | ||
|
|
779d685335 | ||
|
|
4d1e798c56 | ||
|
|
a598035236 | ||
|
|
50395aa821 | ||
|
|
6d9c8fd85b | ||
|
|
b65d994397 | ||
|
|
b20258c66e | ||
|
|
9984238f2a | ||
|
|
c3599bacf2 | ||
|
|
c10481b99d | ||
|
|
02e01b7386 | ||
|
|
ca7147a0a7 | ||
|
|
022c360964 | ||
|
|
d41f4d2db3 | ||
|
|
4871682dc6 | ||
|
|
1cebc1248b | ||
|
|
70d235f99e | ||
|
|
153409d37e | ||
|
|
67a30fdf91 | ||
|
|
911ce3f047 | ||
|
|
c97f7041a7 | ||
|
|
89bfb4f722 | ||
|
|
da3935ff36 | ||
|
|
06810bff91 | ||
|
|
a9af75bbd1 | ||
|
|
da2bff691b | ||
|
|
a85b3aa69f | ||
|
|
859f89101e | ||
|
|
2545912532 | ||
|
|
2fc1519bc6 | ||
|
|
be6ef15ffa | ||
|
|
5666079d92 | ||
|
|
85f5ba9a25 | ||
|
|
df141a48d9 | ||
|
|
fed6fc131b | ||
|
|
cf49891853 | ||
|
|
de2a894269 | ||
|
|
34d10f8db7 | ||
|
|
68f76465cf | ||
|
|
5f91413023 | ||
|
|
45d6955260 | ||
|
|
3b801d43bc | ||
|
|
30df060357 | ||
|
|
252be84bf0 | ||
|
|
40ab272995 | ||
|
|
0ec3b5db8b | ||
|
|
55e07c1c9a | ||
|
|
7e475511b6 | ||
|
|
ca77e4545a | ||
|
|
5dc9723585 | ||
|
|
3f8350b78f | ||
|
|
500a56b869 | ||
|
|
e50b7174bf | ||
|
|
8299c735b1 | ||
|
|
124878ed22 | ||
|
|
1c09570805 | ||
|
|
e56797737d | ||
|
|
81852de1fa | ||
|
|
fbd39da8ca | ||
|
|
db0ef95fe3 | ||
|
|
d888aa67f9 | ||
|
|
0cb3e1749b | ||
|
|
b5a51aced3 | ||
|
|
04ba57cb38 | ||
|
|
ba42ad2115 | ||
|
|
5c13382071 | ||
|
|
3df8c63d5c | ||
|
|
8ff10d5995 | ||
|
|
63ac8863f3 | ||
|
|
2047a16a57 | ||
|
|
8d6d55d628 | ||
|
|
9a7dd5ea19 | ||
|
|
30b544a743 | ||
|
|
a0bb7b08bd | ||
|
|
e1d655cb23 | ||
|
|
5d46a112f8 | ||
|
|
a1b1e261de | ||
|
|
e01e5e6b0e | ||
|
|
4f2dc50b5c | ||
|
|
c3e5106b04 | ||
|
|
593ae295e3 | ||
|
|
9d50b78b69 | ||
|
|
198170ca48 | ||
|
|
00986fd7bf | ||
|
|
2519ebff9d | ||
|
|
c33775d71e | ||
|
|
6b0721cc21 | ||
|
|
d306e76420 | ||
|
|
73cd76932a | ||
|
|
5d19a9f50f | ||
|
|
368c30ef9d | ||
|
|
f77bf72de8 | ||
|
|
f2fbd92f78 | ||
|
|
480134302f | ||
|
|
74cc8c022c | ||
|
|
c6f3b00f0e | ||
|
|
626fbd1d10 | ||
|
|
b8df05cf88 | ||
|
|
e56fc4b1ee | ||
|
|
76ebd2d34f | ||
|
|
85c273c8ca | ||
|
|
b9cd834e95 | ||
|
|
cfa6bee081 | ||
|
|
b46cc2c1a9 | ||
|
|
5e734fc5a6 | ||
|
|
d08bc7767e | ||
|
|
bbd0761887 | ||
|
|
4668149943 | ||
|
|
a5b7c057eb | ||
|
|
0a04bb110a | ||
|
|
9c0b414676 | ||
|
|
5be4b0c4fd | ||
|
|
8e0eab20e2 | ||
|
|
1b5aa67f5d | ||
|
|
02c278f393 | ||
|
|
54472b3806 | ||
|
|
14e5f89feb | ||
|
|
a0b93bb4df | ||
|
|
85347d9675 | ||
|
|
060f7a7191 | ||
|
|
81ca6f7cba | ||
|
|
515645bb4d | ||
|
|
6ae6209457 | ||
|
|
272325ff05 | ||
|
|
a84b49b92d | ||
|
|
b63efdd80b | ||
|
|
2afa6ed2cb | ||
|
|
34f1a9ebfb | ||
|
|
6f2f8697ae | ||
|
|
6ec9d1d6ce | ||
|
|
047f22e110 | ||
|
|
a91fcbb310 | ||
|
|
5fc3eac0f6 | ||
|
|
937faaf149 | ||
|
|
115e7d6e50 | ||
|
|
34259d5d9d | ||
|
|
91b6ebb0c0 | ||
|
|
60b4f1f89f | ||
|
|
023ff6834a | ||
|
|
d00cab0b0a | ||
|
|
17493ff655 | ||
|
|
6c27e6c4fe | ||
|
|
4cb0be4df3 | ||
|
|
40f555a3b6 | ||
|
|
881fdbe893 | ||
|
|
75d840fa1a | ||
|
|
800bdd8fc5 | ||
|
|
b04ade8060 | ||
|
|
7f4d96f33e | ||
|
|
9d8daca54d | ||
|
|
bb898b719f | ||
|
|
3a0b57b50f | ||
|
|
38d2333165 | ||
|
|
a7f0b5adb3 | ||
|
|
109fff0fa6 | ||
|
|
8c591e7248 | ||
|
|
0dafa10e27 | ||
|
|
60e3ff0bf0 | ||
|
|
6052405eb7 | ||
|
|
17a45109f4 | ||
|
|
392b45e6cb | ||
|
|
f27e1ba885 | ||
|
|
990c9d6f7c | ||
|
|
b301b8f6f2 | ||
|
|
924cf9bfd4 | ||
|
|
7e6573d191 | ||
|
|
fdae3a3f31 | ||
|
|
51194e20f2 | ||
|
|
043f2cb214 | ||
|
|
651d462456 | ||
|
|
d16d89028f | ||
|
|
baa277c592 | ||
|
|
007f930fed | ||
|
|
f253f164a3 | ||
|
|
abfe71bb04 | ||
|
|
0c645b12d1 | ||
|
|
f824d1eb3b | ||
|
|
afbbe1148f | ||
|
|
8403e91fc9 | ||
|
|
c300879995 | ||
|
|
ea1d298eb3 | ||
|
|
d17b36519a | ||
|
|
f4f0896470 | ||
|
|
2c9b41db75 | ||
|
|
a38cd449c5 | ||
|
|
977cad3830 | ||
|
|
987587b5f3 | ||
|
|
9351f686b1 | ||
|
|
42e557bd3f | ||
|
|
35c2c8e8de | ||
|
|
960e1d49c2 | ||
|
|
100d979d3e | ||
|
|
b734599f40 | ||
|
|
b41b4fc584 | ||
|
|
b69bf58c63 | ||
|
|
a73e598a6e | ||
|
|
af2de6776d | ||
|
|
98195c9e74 | ||
|
|
97188ad85b | ||
|
|
9c673d2a2e | ||
|
|
77182d8711 | ||
|
|
a01c9ff379 | ||
|
|
32399b0cb5 | ||
|
|
fe3cdb1e20 | ||
|
|
3d25cecc69 | ||
|
|
068c0ff36c | ||
|
|
86a298792a | ||
|
|
a6599874db | ||
|
|
0deae84ecb | ||
|
|
6243fbfd0d | ||
|
|
f49d7ce1da | ||
|
|
75fed52bb6 | ||
|
|
90ec177360 | ||
|
|
667eadb430 | ||
|
|
afbe0bc307 | ||
|
|
c65af6a74f | ||
|
|
669aee2d73 | ||
|
|
67f0dfa449 | ||
|
|
e630a1ace1 | ||
|
|
21485ca6e2 | ||
|
|
87fb4af5cb | ||
|
|
e86ec75513 | ||
|
|
1ad099d9fd | ||
|
|
f9a677c1a3 | ||
|
|
6c6b67330f | ||
|
|
2b33e78fd3 | ||
|
|
b65e9fe0f5 | ||
|
|
571b817f04 | ||
|
|
e2b2815545 | ||
|
|
0ea58ab268 | ||
|
|
2fbb39bf6f | ||
|
|
f4c87b3739 | ||
|
|
696d91daa3 | ||
|
|
9a1781e6e7 | ||
|
|
3395b51086 | ||
|
|
a3a77a0bb6 | ||
|
|
d35a2b0faa | ||
|
|
88c57d002d | ||
|
|
0d9ff907a8 | ||
|
|
02d8897b82 | ||
|
|
0015484679 | ||
|
|
aa5aa798e6 | ||
|
|
63a4a70420 | ||
|
|
2badb04a03 | ||
|
|
a328e12642 | ||
|
|
ce9f1eb201 | ||
|
|
b454ac9ce5 | ||
|
|
a010f7439d | ||
|
|
d7b0ba57e0 | ||
|
|
2ca161f3d8 | ||
|
|
79fdf641c0 | ||
|
|
89194c067b | ||
|
|
b5ab498e75 | ||
|
|
126a5e5e4e | ||
|
|
16d1f9aca8 | ||
|
|
466ab0b404 | ||
|
|
a52a871a94 | ||
|
|
4f9379a5e2 | ||
|
|
4cfad2ef3b | ||
|
|
6e81b0ac32 | ||
|
|
d87023e1ce | ||
|
|
e31d2c9857 | ||
|
|
2174ccf016 | ||
|
|
8f1607e010 | ||
|
|
e93129f1ae | ||
|
|
35b4e190d9 | ||
|
|
b252f52f8e | ||
|
|
eba30c4d79 | ||
|
|
ae1662e433 | ||
|
|
512a3bf09a | ||
|
|
9927ce14d3 | ||
|
|
e5e169f476 | ||
|
|
81df837a33 | ||
|
|
b84b526a43 | ||
|
|
5fc92ab0e6 | ||
|
|
039c2a6d57 | ||
|
|
d053422657 | ||
|
|
90d9eb3520 | ||
|
|
3466da4338 | ||
|
|
17c1a1e465 | ||
|
|
6872ab0e14 | ||
|
|
136d16b7fd | ||
|
|
d651deb31c | ||
|
|
8fb37233f4 | ||
|
|
700194b80d | ||
|
|
ef2dd673ec | ||
|
|
7a3adca771 | ||
|
|
f66b0b7fea | ||
|
|
b6df0209ba | ||
|
|
6b53d263fe | ||
|
|
00024240bb | ||
|
|
237a889f54 | ||
|
|
f5c265b7b5 | ||
|
|
1341ab5703 | ||
|
|
3ded8196c4 | ||
|
|
cbc239ceaa | ||
|
|
a6b43baafe | ||
|
|
ba0a261840 | ||
|
|
3bb0d523d3 | ||
|
|
85cb333a5d | ||
|
|
2abb073b3a | ||
|
|
0634386b2e | ||
|
|
372e9335b1 | ||
|
|
e6b6082a2b | ||
|
|
e0ee0b9254 | ||
|
|
fde47133da | ||
|
|
643f64df3f | ||
|
|
a6ec1ba23b | ||
|
|
ae231b1d1b | ||
|
|
ecdf66c454 | ||
|
|
aaca28c1a6 | ||
|
|
f0d8fdf2d8 | ||
|
|
c9288a052a | ||
|
|
4b3a425888 | ||
|
|
71069ced7f | ||
|
|
1308839a41 | ||
|
|
6d849ad5f6 | ||
|
|
fa95191792 | ||
|
|
e945aafd7b | ||
|
|
e9f75adddb | ||
|
|
3e4b66675b | ||
|
|
6f8d3f7a57 | ||
|
|
eb994a691a | ||
|
|
6ebd8e78c8 | ||
|
|
daa947e7eb | ||
|
|
b117130aac | ||
|
|
4587aba1d4 | ||
|
|
ae73e6f04d | ||
|
|
5930a64203 | ||
|
|
97536c4e9b | ||
|
|
e81e33af38 | ||
|
|
a7c56eab86 | ||
|
|
6e580a731e | ||
|
|
7843a6bf77 | ||
|
|
fa6d933544 | ||
|
|
83085d08ca | ||
|
|
a02055e9b5 | ||
|
|
9acd792abe | ||
|
|
4ef55502b4 | ||
|
|
3009863877 | ||
|
|
70a6b171d0 | ||
|
|
9a25b5458d | ||
|
|
416aeb4072 | ||
|
|
0c1594b8f8 | ||
|
|
669ae104a4 | ||
|
|
c37cfeb74f | ||
|
|
51b1da660a | ||
|
|
c394985b1b | ||
|
|
04c300b8e2 | ||
|
|
117f33afc5 | ||
|
|
3d92669df5 | ||
|
|
63757af1a0 | ||
|
|
e6a58b6700 | ||
|
|
8fb504c963 | ||
|
|
babe42ef35 | ||
|
|
b67e3ff957 | ||
|
|
7557220d5d | ||
|
|
50ccccc3f4 | ||
|
|
b6afc68847 | ||
|
|
d06813f528 | ||
|
|
36d5debe74 | ||
|
|
a7d5fb5006 | ||
|
|
dadfcd4f81 | ||
|
|
236229ea7e | ||
|
|
6f0f3cc115 | ||
|
|
3aa072b437 | ||
|
|
930b15ae09 | ||
|
|
1c1f4068cd | ||
|
|
b6690e0012 | ||
|
|
52e7f670f3 | ||
|
|
12d643694f | ||
|
|
3876a96b67 | ||
|
|
e50f234ba3 | ||
|
|
fd6df8e52a | ||
|
|
28225c7cbd | ||
|
|
cab2929c10 | ||
|
|
52a490bf5d | ||
|
|
f2c16fbf3c | ||
|
|
65a633f42d | ||
|
|
d2c6d79589 | ||
|
|
f4a873745f | ||
|
|
c7e9d90321 | ||
|
|
7476194bd1 | ||
|
|
1770c85689 | ||
|
|
4544893b4c | ||
|
|
f30fa925ea | ||
|
|
beb91559e5 | ||
|
|
fbc5e74318 | ||
|
|
e364659c6e | ||
|
|
9e26198afe | ||
|
|
f9b6ddc230 | ||
|
|
0991f94d06 | ||
|
|
32513083b1 | ||
|
|
336cdcddc5 | ||
|
|
4047c1a4e4 | ||
|
|
091cf390d2 | ||
|
|
05aaafc1cf | ||
|
|
5885b833cd | ||
|
|
106627da04 | ||
|
|
d73ea54e08 | ||
|
|
a45bfaf3da | ||
|
|
e85cc0d856 | ||
|
|
0f608f3a15 | ||
|
|
4ad5c6f864 | ||
|
|
be47b6a6c0 | ||
|
|
1f982c94ce | ||
|
|
12472a2612 | ||
|
|
f6a8d32880 | ||
|
|
bb2f86463e | ||
|
|
e8dafc02f7 | ||
|
|
0655834938 | ||
|
|
d0dc505220 | ||
|
|
b2d3f3ff22 | ||
|
|
39730b6834 | ||
|
|
dd1991f2c6 | ||
|
|
2f32e11f53 | ||
|
|
280f55a875 | ||
|
|
dc68be5abf | ||
|
|
1ef90902bd | ||
|
|
6f37e97c67 | ||
|
|
e54c74d972 | ||
|
|
af9fa85cc1 | ||
|
|
74828e1409 | ||
|
|
dc77400ab1 | ||
|
|
2d0638821d | ||
|
|
8a87d60f29 | ||
|
|
530789b733 | ||
|
|
618d75566f | ||
|
|
5f66893038 | ||
|
|
41ea433e7c | ||
|
|
bfd7881b7b | ||
|
|
1f9806a480 | ||
|
|
5b43a108bc | ||
|
|
b253c8cc95 | ||
|
|
0fc9ed852e | ||
|
|
175c1f2720 | ||
|
|
dafdbc9ddb | ||
|
|
14f5204548 | ||
|
|
5233463f0b | ||
|
|
1d4a416100 | ||
|
|
25ee796d5b | ||
|
|
e08107063a | ||
|
|
30d1605007 | ||
|
|
cd5a86bfcf | ||
|
|
a23da9f867 | ||
|
|
d5bb37b552 | ||
|
|
97b67d0f93 | ||
|
|
3f82be7192 | ||
|
|
adfcb5f7b6 | ||
|
|
5940feb64b | ||
|
|
229e6809d8 | ||
|
|
87e5687d03 | ||
|
|
5aba1d9aec | ||
|
|
68ebe85a98 | ||
|
|
789ac5dfd4 | ||
|
|
ceb8fee0cc | ||
|
|
421270f4a6 | ||
|
|
afdf5750b5 | ||
|
|
ea869d4ffc | ||
|
|
9d89eed873 | ||
|
|
c00eea7991 | ||
|
|
ba8f48af65 | ||
|
|
9930e2745f | ||
|
|
da3879e928 | ||
|
|
7195b7c803 | ||
|
|
9b082eea14 | ||
|
|
a16218b311 | ||
|
|
f427c00d94 | ||
|
|
8bcd8c404d | ||
|
|
4d7f9c42c8 | ||
|
|
29a71fd903 | ||
|
|
cd263484c3 | ||
|
|
fcacac7c6f | ||
|
|
78d74261e9 | ||
|
|
16d694734b | ||
|
|
252ab0fbab | ||
|
|
8eb9c451a1 | ||
|
|
469c52be28 | ||
|
|
54fa51eeff | ||
|
|
5456af6867 | ||
|
|
180446c34d | ||
|
|
3a2c5b318a | ||
|
|
cfff69a715 | ||
|
|
08883d86ef | ||
|
|
8a849ebeff | ||
|
|
05a796faf1 | ||
|
|
9e1d03b383 | ||
|
|
0a929f2971 | ||
|
|
7878992570 | ||
|
|
4f95926cbd | ||
|
|
f3e997ea39 | ||
|
|
2b921c21ff | ||
|
|
9736d63577 | ||
|
|
13add414c4 | ||
|
|
b032bc13db | ||
|
|
aaad428438 | ||
|
|
203895fc7e | ||
|
|
aab1fab445 | ||
|
|
e06221bc89 | ||
|
|
26a13edcf3 | ||
|
|
65b6fe576f | ||
|
|
4671829ad8 | ||
|
|
293be752ca | ||
|
|
0a6e4f31d5 | ||
|
|
e6c4ce51f7 | ||
|
|
3924063060 | ||
|
|
d122f9f700 | ||
|
|
d0649ba815 | ||
|
|
1ec09270a7 | ||
|
|
1ddd7415cb | ||
|
|
ec9d0d4008 | ||
|
|
08c8bd3049 | ||
|
|
2520d9f400 | ||
|
|
0e863ff9ca | ||
|
|
1b78f54c6b | ||
|
|
b732c24ec4 | ||
|
|
af604aba31 | ||
|
|
c82658440f | ||
|
|
7e660d4d8e | ||
|
|
4a8147f8a5 | ||
|
|
583830c652 | ||
|
|
95fdb549d7 | ||
|
|
a598f0e632 | ||
|
|
293dbd8a8b | ||
|
|
f03a378ce0 | ||
|
|
6aae8aee5b | ||
|
|
6d908d3e79 | ||
|
|
d5016c7133 | ||
|
|
b5a1b692bd | ||
|
|
834c396a22 | ||
|
|
bc18d241e8 | ||
|
|
5a1877087f | ||
|
|
50462ec15d | ||
|
|
1dd5e2c926 | ||
|
|
ebddc46bc0 | ||
|
|
138cbf9761 | ||
|
|
f21c6bca00 | ||
|
|
9aad8a7774 | ||
|
|
68b6c7d886 | ||
|
|
1c489e57cc | ||
|
|
6719578f14 | ||
|
|
d5587de316 | ||
|
|
77f28e3441 | ||
|
|
3fa63b774e | ||
|
|
713c7cd8e3 | ||
|
|
e6b4d87939 | ||
|
|
27c94d9874 | ||
|
|
eece8a0e26 | ||
|
|
fb85867d72 | ||
|
|
c454bfcd84 | ||
|
|
769232f368 | ||
|
|
9cf10eecd1 | ||
|
|
f927d5b8f5 | ||
|
|
7fa696dace | ||
|
|
feac93389c | ||
|
|
f7969d91b3 | ||
|
|
92aafb9043 | ||
|
|
f9328d53b4 | ||
|
|
f1cbc7da33 | ||
|
|
01becd21de | ||
|
|
7768b94279 | ||
|
|
3bc51c8e69 | ||
|
|
d206be91d5 | ||
|
|
6e69c9e375 | ||
|
|
f2846af4ec | ||
|
|
657eed1dc9 | ||
|
|
e351ab0171 | ||
|
|
779446da64 | ||
|
|
2ff0d7aa83 | ||
|
|
7ceb64b57b | ||
|
|
a1c12cfd77 | ||
|
|
aa6ca21a34 | ||
|
|
a49521d683 | ||
|
|
3be6e5b015 | ||
|
|
ca1725b98c | ||
|
|
d11dfe2ced | ||
|
|
ab30ba1e1b | ||
|
|
7f23cb9bf5 | ||
|
|
c9d3cf301e | ||
|
|
67282882fa | ||
|
|
73bf4f45c3 | ||
|
|
66ae62fb91 | ||
|
|
8bae804508 | ||
|
|
d87acc97c3 | ||
|
|
f9b2c59974 | ||
|
|
a870a3b918 | ||
|
|
008ed34553 | ||
|
|
e239045688 | ||
|
|
ed80bfaf02 | ||
|
|
473b35f9a3 | ||
|
|
45bb7eec0b | ||
|
|
58bb029666 | ||
|
|
0f97478b55 | ||
|
|
9efa70a551 | ||
|
|
ed65721085 | ||
|
|
83688fceb7 | ||
|
|
088f75ba0c | ||
|
|
188cfa08a9 | ||
|
|
b89bd24bed | ||
|
|
effda88b51 | ||
|
|
3844f70a4d | ||
|
|
8e333757f9 | ||
|
|
0fb12bcc9c | ||
|
|
44d78ef92a | ||
|
|
ebb6729a26 | ||
|
|
a35f8bddde | ||
|
|
8fbe7ba742 | ||
|
|
f039b0b6e9 | ||
|
|
9ad9ef7957 | ||
|
|
5c7db04465 | ||
|
|
838105fb65 | ||
|
|
5ca87c0f20 | ||
|
|
af4edff370 | ||
|
|
f40c048475 | ||
|
|
77247cccbe | ||
|
|
fcfcd77bfd | ||
|
|
b3667befb4 | ||
|
|
a6cb0e0a96 | ||
|
|
c047f943de | ||
|
|
79089cc47e | ||
|
|
3c631902e1 | ||
|
|
379c24a012 | ||
|
|
4035b87693 | ||
|
|
11d1a8c3cf | ||
|
|
7eb9c8265c | ||
|
|
572beb2311 | ||
|
|
d861d8bfb8 | ||
|
|
6791ff6192 | ||
|
|
9d9de6b2a3 | ||
|
|
1f7ef15ad1 | ||
|
|
16c582ec7a | ||
|
|
de58d0ecca | ||
|
|
010f6c7f1a | ||
|
|
aea5612c39 | ||
|
|
b8b912bdd5 | ||
|
|
e4ca88726e | ||
|
|
616f109671 | ||
|
|
8e0580ff96 | ||
|
|
4b2e7620dd | ||
|
|
b82f25c503 | ||
|
|
c174c0cc6d | ||
|
|
117da337c7 | ||
|
|
01da46f753 | ||
|
|
d17efce4f5 | ||
|
|
e7a6d1f532 | ||
|
|
f643f2c601 | ||
|
|
480faa6461 | ||
|
|
1fa084b6be | ||
|
|
1c86b00b5c | ||
|
|
10823e1c37 | ||
|
|
f73693206f | ||
|
|
861c8b29c0 | ||
|
|
5037046624 | ||
|
|
5c0614d656 | ||
|
|
697866d1ba | ||
|
|
38d826d152 | ||
|
|
13cc29cd8c | ||
|
|
401357b8cb | ||
|
|
599e1bb220 | ||
|
|
864fa17b75 | ||
|
|
a98c9ed0af | ||
|
|
8032aa1ad9 | ||
|
|
5ff4e3b194 | ||
|
|
b01bf6089c | ||
|
|
f9a33bfc14 | ||
|
|
610b412506 | ||
|
|
09000ad9b3 | ||
|
|
f70f0f8d62 | ||
|
|
d5c3f9e780 | ||
|
|
b42dab3eef | ||
|
|
7cbea49c2d | ||
|
|
6dcc5a1169 | ||
|
|
53129125dd | ||
|
|
2d52b9fb39 | ||
|
|
863cbb785d | ||
|
|
ba1a4f06ff | ||
|
|
cf5be85dad | ||
|
|
d21b67446f | ||
|
|
3b48a270fc | ||
|
|
105e9da866 | ||
|
|
d3b16ba443 | ||
|
|
57fc6a3f50 | ||
|
|
abc51fdc5d | ||
|
|
35a0a658a7 | ||
|
|
2c99a8bee4 | ||
|
|
1dd2bdcb8e | ||
|
|
9f67da00d1 | ||
|
|
82d53a8c3d | ||
|
|
f3eee25527 | ||
|
|
ee11775425 | ||
|
|
bcdf9ac5ca | ||
|
|
4accdf77f8 | ||
|
|
fc46f70153 | ||
|
|
e7cf7d58b8 | ||
|
|
d98e9e1838 | ||
|
|
369d3aa62e | ||
|
|
d4ac6dbfe4 | ||
|
|
91d35905fd | ||
|
|
1a34830f0e | ||
|
|
f000df1e15 | ||
|
|
78b0072051 | ||
|
|
178f7b4643 | ||
|
|
f34a8fff6a | ||
|
|
7766e1f684 | ||
|
|
bde1f6d199 | ||
|
|
0d7ee6f208 | ||
|
|
78adaecb89 | ||
|
|
a18e1a0161 | ||
|
|
4308b8a4a5 | ||
|
|
280d98bad9 | ||
|
|
ae5bf747c9 | ||
|
|
1ae0820ecc | ||
|
|
c09473f41e | ||
|
|
99a3e0c399 | ||
|
|
d2bd4a213b | ||
|
|
1dcb0b52e2 | ||
|
|
409c9c4e23 | ||
|
|
aa54e14c37 | ||
|
|
3b2c74042e | ||
|
|
11ae938146 | ||
|
|
f11bb254a5 | ||
|
|
5187138547 | ||
|
|
2bb6387dae | ||
|
|
ca293dc0e7 | ||
|
|
ea1d4e7f50 | ||
|
|
0b681c471e | ||
|
|
80267aa418 | ||
|
|
36c31a21b9 | ||
|
|
51725d3d9c | ||
|
|
05d3354570 | ||
|
|
8799a15e73 | ||
|
|
2cde9a82a0 | ||
|
|
2c1fa628a2 | ||
|
|
a67fc64afb | ||
|
|
6bbdc2bae1 | ||
|
|
50d7fd776f | ||
|
|
1c38f705a7 | ||
|
|
b643939cc4 | ||
|
|
3ed3e93b25 | ||
|
|
f6ea09e581 | ||
|
|
998f89216e | ||
|
|
aefc6ff7b4 | ||
|
|
66615f1a96 | ||
|
|
a5dc91c175 | ||
|
|
d04436aa0a | ||
|
|
6542b8b198 | ||
|
|
6813787fc7 | ||
|
|
afdb24610d | ||
|
|
58e4bf1cc3 | ||
|
|
28761fc960 | ||
|
|
69e54ab410 | ||
|
|
116ceb6f93 | ||
|
|
5d022a575a | ||
|
|
e8fd0f3531 | ||
|
|
8103c399d5 | ||
|
|
cf3e7f90d6 | ||
|
|
22bfac746e | ||
|
|
066a3b8b52 | ||
|
|
48141c0693 | ||
|
|
576e21eb65 | ||
|
|
a51f5edbc8 | ||
|
|
ef59f38ec4 | ||
|
|
47120fae01 | ||
|
|
93a4327921 | ||
|
|
0f2bbd7bfd | ||
|
|
c0417c1989 | ||
|
|
fb6cfa45fd | ||
|
|
b875cea10d | ||
|
|
516372e5db | ||
|
|
0899a1052e | ||
|
|
32bf17c076 | ||
|
|
66a6a8f33c | ||
|
|
007fe6a030 | ||
|
|
f5f9491811 | ||
|
|
04e09c0078 | ||
|
|
05b71564d8 | ||
|
|
1791a5bb11 | ||
|
|
3e6a99fc22 | ||
|
|
a5419ecc5c | ||
|
|
a36b138efe | ||
|
|
6d30fdb83d | ||
|
|
5c4741c5d4 | ||
|
|
93c748bd3c | ||
|
|
7ba6e320e7 | ||
|
|
54468ab1a8 | ||
|
|
01f5435f63 | ||
|
|
22768ff6c6 | ||
|
|
122526a9d0 | ||
|
|
6cb36a6cee | ||
|
|
925afe0999 | ||
|
|
f743410b4e | ||
|
|
4a2206ecb1 | ||
|
|
ffde2c96c7 | ||
|
|
2bd46230be | ||
|
|
b04fe21d65 | ||
|
|
266f9cc370 | ||
|
|
1682de59df | ||
|
|
42fd14f5c0 | ||
|
|
1988c02b7f | ||
|
|
517eaa8b80 | ||
|
|
1f78462f58 | ||
|
|
36bbcc8559 | ||
|
|
f26253ec49 | ||
|
|
f2dc287f14 | ||
|
|
3fe3151af7 | ||
|
|
1c1fd8f210 | ||
|
|
671d53877a | ||
|
|
97710a4576 | ||
|
|
c08fae8bce | ||
|
|
f02dd2f439 | ||
|
|
e544f1fa1e | ||
|
|
130ff27f26 | ||
|
|
79a9ac3bc8 | ||
|
|
c5308d51f4 | ||
|
|
a6f4de5817 | ||
|
|
8825a03033 | ||
|
|
abdfc5c597 | ||
|
|
3ce2f0d100 | ||
|
|
be2faaa110 | ||
|
|
f33269e50b | ||
|
|
bbc355df07 | ||
|
|
d58f9031d1 | ||
|
|
0312016f89 | ||
|
|
e3ae013e42 | ||
|
|
07a2b136b8 | ||
|
|
3d76a982aa | ||
|
|
92d726bbd4 | ||
|
|
e2ef0bc3a6 | ||
|
|
13c29cb7a9 | ||
|
|
b22c6a0078 | ||
|
|
f4784412de | ||
|
|
33c5ea1f4e | ||
|
|
3dc15068b9 | ||
|
|
4cb30f1ce4 | ||
|
|
b868de8d67 | ||
|
|
04aedcc056 | ||
|
|
d9f1bcbf15 | ||
|
|
105d17748e | ||
|
|
dd27950fae | ||
|
|
4b7af8d3b4 | ||
|
|
f3fd82a24a | ||
|
|
cd97b2fb96 | ||
|
|
f661c233be | ||
|
|
6a2a2d5d11 | ||
|
|
87ff433ef8 | ||
|
|
9e4e3a8dfa | ||
|
|
4d4441217f | ||
|
|
7e51ca9912 | ||
|
|
94a29be415 | ||
|
|
9dfda83946 | ||
|
|
41826fc3cb | ||
|
|
0ed13f6943 | ||
|
|
6c2ed1be22 | ||
|
|
ddec424429 | ||
|
|
d68b34cefe | ||
|
|
7e6d061646 | ||
|
|
c19725506d | ||
|
|
a6ceaf8d96 | ||
|
|
f43fbffdf7 | ||
|
|
68c099a2af | ||
|
|
70a05b4280 | ||
|
|
094974d417 | ||
|
|
d89314a559 | ||
|
|
086340540a | ||
|
|
ed83b1d9e9 | ||
|
|
4f6d2a8b71 | ||
|
|
d58a8ebba0 | ||
|
|
6be465fe9b | ||
|
|
26225aff57 | ||
|
|
fd55360672 | ||
|
|
4e766c7c3b | ||
|
|
0b10d98e0b | ||
|
|
be0a3fb1f2 | ||
|
|
02e89d77bb | ||
|
|
4b753b1610 | ||
|
|
f8381628d4 | ||
|
|
d8f41f67c9 | ||
|
|
ee2d0b963d | ||
|
|
f051c0e564 | ||
|
|
03d3bbcddb | ||
|
|
06cafb09b3 | ||
|
|
a7a7b956b1 | ||
|
|
9b39ba169c | ||
|
|
4670929953 | ||
|
|
366e2e7a94 | ||
|
|
e5f05ca9be | ||
|
|
d08522408a | ||
|
|
cc31c8fc33 | ||
|
|
db60e8868c | ||
|
|
1adae67dd7 | ||
|
|
5ad3044314 | ||
|
|
90fe556e5f | ||
|
|
c0152940f9 | ||
|
|
9313ba08ed | ||
|
|
f3b9930dea | ||
|
|
5520144ff4 | ||
|
|
2b9ea58c86 | ||
|
|
8f42f59a80 | ||
|
|
c6970e1998 | ||
|
|
c61bae3a33 | ||
|
|
b0f9035e2d | ||
|
|
aba9748ffd | ||
|
|
2876ef7607 | ||
|
|
7d1aeede1a | ||
|
|
b7f4a11eee | ||
|
|
0e5138d6ec | ||
|
|
f1518226bd | ||
|
|
4d26fc7e7c | ||
|
|
102cf52a16 | ||
|
|
198ed859ff | ||
|
|
9d44d5d4e7 | ||
|
|
e8896fe238 | ||
|
|
21281789e0 | ||
|
|
b71566f206 | ||
|
|
181539651f | ||
|
|
b69564f5c9 | ||
|
|
c26e00b5bd | ||
|
|
dc606645fd | ||
|
|
0e04d20762 | ||
|
|
7040086201 | ||
|
|
6f3c3b6d61 | ||
|
|
37f250ddc1 | ||
|
|
35f310885e | ||
|
|
616ca4fe1f | ||
|
|
a9fe39459a | ||
|
|
1e1dd8c668 | ||
|
|
585e08eb95 | ||
|
|
a42eeb12d2 | ||
|
|
cf66f67fb6 | ||
|
|
2408d78f47 | ||
|
|
4f8a5eb1a0 | ||
|
|
06e5966cb4 | ||
|
|
ea51f1c896 | ||
|
|
77e5450746 | ||
|
|
6e10fea119 | ||
|
|
f52c247bd5 | ||
|
|
0dd857f7a2 | ||
|
|
d817990283 | ||
|
|
9905099a71 | ||
|
|
0eba5a0de3 | ||
|
|
5eb3c1a67b | ||
|
|
b370375414 | ||
|
|
8536f6c163 | ||
|
|
bb1f97abc2 | ||
|
|
e1cd846c9a | ||
|
|
1fcc2b0029 | ||
|
|
f4f41a5985 | ||
|
|
173a6eee03 | ||
|
|
d9e4017677 | ||
|
|
7beac0b105 | ||
|
|
f0fef94a4f | ||
|
|
78cd4481e4 | ||
|
|
af3c9eaec1 | ||
|
|
0cf029edd4 | ||
|
|
c0dac1383d | ||
|
|
a3d0d4a5bf | ||
|
|
12d263999b | ||
|
|
fa900d5dbb | ||
|
|
ddc2c8d110 | ||
|
|
c91f41e984 | ||
|
|
acfba410dd | ||
|
|
b8ca530c55 | ||
|
|
b31c097531 | ||
|
|
0f9fe8648e | ||
|
|
791a641eef | ||
|
|
c5fba24cc5 | ||
|
|
b8b2ea7ccb | ||
|
|
0b228ed6d3 | ||
|
|
062a5bfe8d |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
48
.github/ISSUE_TEMPLATE.md
vendored
Normal file
48
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
Before opening a new issue, please search through the existing issues to
|
||||
see if your topic has already been addressed. Note that you may need to
|
||||
remove the "is:open" filter from the search bar to include closed issues.
|
||||
|
||||
Check the appropriate type for your issue below by placing an x between the
|
||||
brackets. For assistance with installation issues, or for any other issues
|
||||
other than those listed below, please raise your topic for discussion on
|
||||
our mailing list:
|
||||
|
||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
|
||||
Please note that issues which do not fall under any of the below categories
|
||||
will be closed. Due to an excessive backlog of feature requests, we are
|
||||
not currently accepting any proposals which extend NetBox's feature scope.
|
||||
|
||||
Do not prepend any sort of tag to your issue's title. An administrator will
|
||||
review your issue and assign labels as appropriate.
|
||||
--->
|
||||
### Issue type
|
||||
[ ] Feature request <!-- An enhancement of existing functionality -->
|
||||
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
||||
[ ] Documentation <!-- A modification to the documentation -->
|
||||
|
||||
<!--
|
||||
Please describe the environment in which you are running NetBox. (Be sure
|
||||
to verify that you are running the latest stable release of NetBox before
|
||||
submitting a bug report.) If you are submitting a bug report and have made
|
||||
any changes to the code base, please first validate that your bug can be
|
||||
recreated while running an official release.
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
* NetBox version: <!-- Example: 2.1.3 -->
|
||||
|
||||
<!--
|
||||
BUG REPORTS must include:
|
||||
* A list of the steps needed for someone else to reproduce the bug
|
||||
* A description of the expected and observed behavior
|
||||
* Any relevant error messages (screenshots may also help)
|
||||
|
||||
FEATURE REQUESTS must include:
|
||||
* A detailed description of the proposed functionality
|
||||
* A use case for the new feature
|
||||
* A rough description of any necessary changes to the database schema
|
||||
* Any relevant third-party libraries which would be needed
|
||||
-->
|
||||
### Description
|
||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
Thank you for your interest in contributing to NetBox! Please note
|
||||
that our contribution policy requires that a feature request or bug
|
||||
report be opened for approval prior to filing a pull request. This
|
||||
helps avoid wasting time and effort on something that we might not
|
||||
be able to accept.
|
||||
|
||||
Please indicate the relevant feature request or bug report below.
|
||||
-->
|
||||
### Fixes:
|
||||
|
||||
<!--
|
||||
Please include a summary of the proposed changes below.
|
||||
-->
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
*.pyc
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/netbox/ldap_config.py
|
||||
/netbox/reports/*
|
||||
!/netbox/reports/__init__.py
|
||||
/netbox/static
|
||||
.idea
|
||||
/*.sh
|
||||
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,23 +1,17 @@
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
env:
|
||||
- DOCKER_TAG=$TRAVIS_TAG
|
||||
|
||||
- postgresql
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pep8
|
||||
before_script:
|
||||
- psql --version
|
||||
- psql -U postgres -c 'SELECT version();'
|
||||
script:
|
||||
- ./scripts/cibuild.sh
|
||||
after_success:
|
||||
- if [ ! -z "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
|
||||
./scripts/docker-build.sh;
|
||||
fi
|
||||
|
||||
133
CONTRIBUTING.md
133
CONTRIBUTING.md
@@ -1,84 +1,117 @@
|
||||
## Getting Help
|
||||
|
||||
If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
|
||||
**do not** open an issue on GitHub except to report bugs or request features.
|
||||
|
||||
### Freenode IRC
|
||||
|
||||
Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using
|
||||
an IRC client, or you can use their [webchat client](https://webchat.freenode.net/).
|
||||
If you encounter any issues installing or using NetBox, try one of the
|
||||
following resources to get assistance. Please **do not** open a GitHub issue
|
||||
except to report bugs or request features.
|
||||
|
||||
### Mailing List
|
||||
|
||||
We have established a Google Groups Mailing List for issues and general discussion. You can find us [here]( https://groups.google.com/forum/#!forum/netbox-discuss).
|
||||
We have established a Google Groups Mailing List for issues and general
|
||||
discussion. This is the best forum for obtaining assistance with NetBox
|
||||
installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
|
||||
|
||||
### Slack
|
||||
|
||||
For real-time discussion, you can join the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/).
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
||||
NetBox. If you're running an older version, it's possible that the bug has already been fixed.
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
|
||||
of NetBox. If you're running an older version, it's possible that the bug has
|
||||
already been fixed.
|
||||
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
|
||||
already been reported. If you think you may be experiencing a reported issue that hasn't already been resolved, please
|
||||
click "add a reaction" in the top right corner of the issue and add a thumbs up (+1). You might also want to add a
|
||||
comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users
|
||||
are affected.
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||
to see if the bug you've found has already been reported. If you think you may
|
||||
be experiencing a reported issue that hasn't already been resolved, please
|
||||
click "add a reaction" in the top right corner of the issue and add a thumbs
|
||||
up (+1). You mightalso want to add a comment describing how it's affecting your
|
||||
installation. This will allow us to prioritize bugs based on how many users are
|
||||
affected.
|
||||
|
||||
* If you haven't found an existing issue that describes your suspected bug, please inquire about it on IRC or Google Groups.
|
||||
**Do not** file an issue until you have received confirmation that it is in fact a bug. Invalid issues are very
|
||||
* If you haven't found an existing issue that describes your suspected bug,
|
||||
please inquire about it on the mailing list. **Do not** file an issue until you
|
||||
have received confirmation that it is in fact a bug. Invalid issues are very
|
||||
distracting and slow the pace at which NetBox is developed.
|
||||
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to include:
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to
|
||||
include:
|
||||
|
||||
* The environment in which NetBox is running
|
||||
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||
* Any error messages returned
|
||||
* Any error messages generated
|
||||
* Screenshots (if applicable)
|
||||
|
||||
* Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may
|
||||
take some time for someone to address your issue.
|
||||
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
|
||||
The issue will be reviewed by a moderator after submission and the appropriate
|
||||
labels will be applied for categorization.
|
||||
|
||||
* Keep in mind that we prioritize bugs based on their severity and how much
|
||||
work is required to resolve them. It may take some time for someone to address
|
||||
your issue.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
||||
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
|
||||
the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue
|
||||
and add a thumbs up. This ensures that the issue has a better chance of making it onto the roadmap. Also feel 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 as spam. Please use GitHub's reactions feature to indicate your support.)
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||
to see if the feature you're requesting is already listed. (Be sure to search
|
||||
closed issues as well, since some feature requests have been rejected.) If the
|
||||
feature you'd like to see has already been requested and is open, click "add a
|
||||
reaction" in the top right corner of the issue and add a thumbs up (+1). This
|
||||
ensures that the issue has a better chance of receiving attention. Also feel
|
||||
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.)
|
||||
|
||||
* While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid
|
||||
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
||||
* Due to an excessive 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.
|
||||
|
||||
* Ticket management
|
||||
* Network state monitoring
|
||||
* Acting as a DNS server
|
||||
* Acting as an authentication server
|
||||
* 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, propose it on IRC or Reddit 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
|
||||
you put into writing a feature request, the better its chance is of being
|
||||
implemented. Overly broad feature requests will be closed.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to enumerate specific functionality and data schema. The more
|
||||
effort you put into writing a feature request, the better its chances are of being implemented. Overly broad feature
|
||||
requests will be closed.
|
||||
|
||||
* When submitting a feature request on GitHub, be sure to include the following:
|
||||
* When submitting a feature request on GitHub, be sure to include the
|
||||
following:
|
||||
|
||||
* A detailed description of the proposed functionality
|
||||
* A use case for the feature; who would use it and what value it would add to NetBox
|
||||
* A rough description of any changes necessary to the database schema
|
||||
* A use case for the feature; who would use it and what value it would add
|
||||
to NetBox
|
||||
* A rough description of changes necessary to the database schema (if
|
||||
applicable)
|
||||
* Any third-party libraries or other resources which would be involved
|
||||
|
||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
|
||||
title. The issue will be reviewed by a moderator after submission and the
|
||||
appropriate labels will be applied for categorization.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
* Be sure to open an issue before starting work on a pull request, and discuss your idea with the NetBox maintainers
|
||||
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
||||
When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
|
||||
* Be sure to open an issue before starting work on a pull request, and discuss
|
||||
your idea with the NetBox maintainers before beginning work. This will help
|
||||
prevent wasting time on something that might we might not be able to implement.
|
||||
When suggesting a new feature, also make sure it won't conflict with any work
|
||||
that's already in progress.
|
||||
|
||||
* When submitting a pull request, please be sure to work off of the `develop` branch, rather than `master`. In NetBox,
|
||||
the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases.
|
||||
* When submitting a pull request, please be sure to work off of the `develop`
|
||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||
development, while `master` is used for tagging new stable releases.
|
||||
|
||||
* All code submissions should meet the following criteria (CI will enforce these checks):
|
||||
* All code submissions should meet the following criteria (CI will enforce
|
||||
these checks):
|
||||
|
||||
* Python syntax is valid
|
||||
* All tests pass when run with `./manage.py test netbox/`
|
||||
* PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length
|
||||
* All tests pass when run with `./manage.py test`
|
||||
* PEP 8 compliance is enforced, with the exception that lines may be
|
||||
greater than 80 characters in length
|
||||
|
||||
## Commenting
|
||||
|
||||
Only comment on an issue if you are sharing a relevant idea or constructive
|
||||
feedback. **Do not** comment on an issue just to show your support (give the
|
||||
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
|
||||
reduce noise in the discussion.
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
||||
FROM python:2.7-wheezy
|
||||
|
||||
WORKDIR /opt/netbox
|
||||
|
||||
ARG BRANCH=master
|
||||
ARG URL=https://github.com/digitalocean/netbox.git
|
||||
RUN git clone --depth 1 $URL -b $BRANCH . && \
|
||||
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz && \
|
||||
pip install gunicorn==17.5 && \
|
||||
pip install django-auth-ldap && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
||||
|
||||
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||
|
||||
ADD docker/gunicorn_config.py /opt/netbox/
|
||||
ADD docker/nginx.conf /etc/netbox-nginx/
|
||||
VOLUME ["/etc/netbox-nginx/"]
|
||||
24
README.md
24
README.md
@@ -1,16 +1,24 @@
|
||||

|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. 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.
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure
|
||||
management (DCIM) tool. 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.
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
||||
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
|
||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
|
||||
|
||||
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
||||
|
||||
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss),
|
||||
or join us in the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/)!
|
||||
|
||||
### Build Status
|
||||
|
||||
| | python 2.7 |
|
||||
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
| **develop** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
@@ -25,9 +33,11 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
|
||||
|
||||
# Installation
|
||||
|
||||
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
||||
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
|
||||
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases)
|
||||
and run `upgrade.sh`.
|
||||
|
||||
## Alternative Installations
|
||||
|
||||
* [Docker container](http://netbox.readthedocs.io/en/stable/installation/docker/)
|
||||
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
|
||||
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.6
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_USER: netbox
|
||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||
POSTGRES_DB: netbox
|
||||
netbox:
|
||||
build: .
|
||||
image: digitalocean/netbox
|
||||
links:
|
||||
- postgres
|
||||
container_name: netbox
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
SUPERUSER_NAME: admin
|
||||
SUPERUSER_EMAIL: admin@example.com
|
||||
SUPERUSER_PASSWORD: admin
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: netbox
|
||||
DB_USER: netbox
|
||||
DB_PASSWORD: J5brHrAXFLQSif0K
|
||||
DB_HOST: postgres
|
||||
SECRET_KEY: r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
EMAIL_SERVER: localhost
|
||||
EMAIL_PORT: 25
|
||||
EMAIL_USERNAME: foo
|
||||
EMAIL_PASSWORD: bar
|
||||
EMAIL_TIMEOUT: 10
|
||||
EMAIL_FROM: netbox@bar.com
|
||||
NETBOX_USERNAME: guest
|
||||
NETBOX_PASSWORD: guest
|
||||
volumes:
|
||||
- netbox-static-files:/opt/netbox/netbox/static
|
||||
nginx:
|
||||
image: nginx:1.11.1-alpine
|
||||
links:
|
||||
- netbox
|
||||
container_name: nginx
|
||||
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
|
||||
depends_on:
|
||||
- netbox
|
||||
ports:
|
||||
- 80:80
|
||||
volumes_from:
|
||||
- netbox
|
||||
volumes:
|
||||
netbox-static-files:
|
||||
driver: local
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# run db migrations (retry on error)
|
||||
while ! /opt/netbox/netbox/manage.py migrate 2>&1; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# create superuser silently
|
||||
if [[ -z ${SUPERUSER_NAME} || -z ${SUPERUSER_EMAIL} || -z ${SUPERUSER_PASSWORD} ]]; then
|
||||
SUPERUSER_NAME='admin'
|
||||
SUPERUSER_EMAIL='admin@example.com'
|
||||
SUPERUSER_PASSWORD='admin'
|
||||
echo "Using defaults: Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}, Password: ${SUPERUSER_PASSWORD}"
|
||||
fi
|
||||
echo "from django.contrib.auth.models import User; User.objects.create_superuser('${SUPERUSER_NAME}', '${SUPERUSER_EMAIL}', '${SUPERUSER_PASSWORD}')" | python /opt/netbox/netbox/manage.py shell
|
||||
|
||||
# copy static files
|
||||
/opt/netbox/netbox/manage.py collectstatic --no-input
|
||||
|
||||
# start unicorn
|
||||
gunicorn --log-level debug --debug --error-logfile /dev/stderr --log-file /dev/stdout -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||
@@ -1,5 +0,0 @@
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
bind = '0.0.0.0:8001'
|
||||
workers = 3
|
||||
user = 'root'
|
||||
@@ -1,35 +0,0 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
server_tokens off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name localhost;
|
||||
|
||||
access_log off;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/netbox/netbox/static/;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://netbox:8001;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# API Integration
|
||||
|
||||
NetBox features a read-only REST API which can be used to integrate it with
|
||||
other applications.
|
||||
|
||||
In the future, both read and write actions will be available via the API.
|
||||
|
||||
## Clients
|
||||
|
||||
The easiest way to start integrating your applications with NetBox is to make
|
||||
use of an API client. If you build or discover an API client that is not part
|
||||
of this list, please send a pull request!
|
||||
|
||||
- **Go**: [github.com/digitalocean/go-netbox](https://github.com/digitalocean/go-netbox)
|
||||
|
||||
## Documentation
|
||||
|
||||
If you wish to build a new API client or simply explore the NetBox API,
|
||||
Swagger documentation can be found at the URL `/api/docs/` on a NetBox server.
|
||||
48
docs/api/authentication.md
Normal file
48
docs/api/authentication.md
Normal file
@@ -0,0 +1,48 @@
|
||||
The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API.
|
||||
|
||||
# Tokens
|
||||
|
||||
A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
|
||||
|
||||
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
|
||||
|
||||
By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
|
||||
|
||||
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
|
||||
|
||||
# Authenticating to the API
|
||||
|
||||
By default, read operations will be available without authentication. In this case, a token may be included in the request, but is not necessary.
|
||||
|
||||
```
|
||||
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
{
|
||||
"count": 10,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
However, if the [`LOGIN_REQUIRED`](../configuration/optional-settings/#login_required) configuration setting has been set to `True`, all requests must be authenticated.
|
||||
|
||||
```
|
||||
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
{
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
```
|
||||
|
||||
To authenticate to the API, set the HTTP `Authorization` header to the string `Token ` (note the trailing space) followed by the token key.
|
||||
|
||||
```
|
||||
$ curl -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
{
|
||||
"count": 10,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, the browsable interface to the API (which can be seen by navigating to the API root `/api/` in a web browser) will attempt to authenticate requests using the same cookie that the normal NetBox front end uses. Thus, if you have logged into NetBox, you will be logged into the browsable API as well.
|
||||
147
docs/api/examples.md
Normal file
147
docs/api/examples.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# API Examples
|
||||
|
||||
Supported HTTP methods:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create a new object
|
||||
* `PUT`: Update an existing object, all mandatory fields must be specified
|
||||
* `PATCH`: Updates an existing object, only specifiying the field to be changed
|
||||
* `DELETE`: Delete an existing object
|
||||
|
||||
To authenticate a request, attach your token in an `Authorization` header:
|
||||
|
||||
```
|
||||
curl -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0"
|
||||
```
|
||||
|
||||
### Retrieving a list of sites
|
||||
|
||||
Send a `GET` request to the object list endpoint. The response contains a paginated list of JSON objects.
|
||||
|
||||
```
|
||||
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
{
|
||||
"count": 14,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Corporate HQ",
|
||||
"slug": "corporate-hq",
|
||||
"region": null,
|
||||
"tenant": null,
|
||||
"facility": "",
|
||||
"asn": null,
|
||||
"physical_address": "742 Evergreen Terrace, Springfield, USA",
|
||||
"shipping_address": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"contact_email": "",
|
||||
"comments": "",
|
||||
"custom_fields": {},
|
||||
"count_prefixes": 108,
|
||||
"count_vlans": 46,
|
||||
"count_racks": 8,
|
||||
"count_devices": 254,
|
||||
"count_circuits": 6
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving a single site by ID
|
||||
|
||||
Send a `GET` request to the object detail endpoint. The response contains a single JSON object.
|
||||
|
||||
```
|
||||
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6/
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Corporate HQ",
|
||||
"slug": "corporate-hq",
|
||||
"region": null,
|
||||
"tenant": null,
|
||||
"facility": "",
|
||||
"asn": null,
|
||||
"physical_address": "742 Evergreen Terrace, Springfield, USA",
|
||||
"shipping_address": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"contact_email": "",
|
||||
"comments": "",
|
||||
"custom_fields": {},
|
||||
"count_prefixes": 108,
|
||||
"count_vlans": 46,
|
||||
"count_racks": 8,
|
||||
"count_devices": 254,
|
||||
"count_circuits": 6
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a new site
|
||||
|
||||
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region."
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site", "region": 5}'
|
||||
{
|
||||
"id": 16,
|
||||
"name": "My New Site",
|
||||
"slug": "my-new-site",
|
||||
"region": 5,
|
||||
"tenant": null,
|
||||
"facility": "",
|
||||
"asn": null,
|
||||
"physical_address": "",
|
||||
"shipping_address": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"contact_email": "",
|
||||
"comments": ""
|
||||
}
|
||||
```
|
||||
Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action.
|
||||
|
||||
### Modify an existing site
|
||||
|
||||
Make an authenticated `PUT` request to the site detail endpoint. As with a create (`POST`) request, all mandatory fields must be included.
|
||||
|
||||
```
|
||||
$ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}'
|
||||
```
|
||||
|
||||
### Modify an object by changing a field
|
||||
|
||||
Make an authenticated `PATCH` request to the device endpoint. With `PATCH`, unlike `POST` and `PUT`, we only specify the field that is being changed. In this example, we add a serial number to a device.
|
||||
```
|
||||
$ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/devices/2549/ --data '{"serial": "FTX1123A090"}'
|
||||
```
|
||||
|
||||
### Delete an existing site
|
||||
|
||||
Send an authenticated `DELETE` request to the site detail endpoint.
|
||||
|
||||
```
|
||||
$ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
|
||||
* Connected to localhost (127.0.0.1) port 8000 (#0)
|
||||
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
||||
> User-Agent: curl/7.35.0
|
||||
> Host: localhost:8000
|
||||
> Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
|
||||
> Content-Type: application/json
|
||||
> Accept: application/json; indent=4
|
||||
>
|
||||
* HTTP 1.0, assume close after body
|
||||
< HTTP/1.0 204 No Content
|
||||
< Date: Mon, 20 Mar 2017 16:13:08 GMT
|
||||
< Server: WSGIServer/0.1 Python/2.7.6
|
||||
< Vary: Accept, Cookie
|
||||
< X-Frame-Options: SAMEORIGIN
|
||||
< Allow: GET, PUT, PATCH, DELETE, OPTIONS
|
||||
<
|
||||
* Closing connection 0
|
||||
```
|
||||
|
||||
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.
|
||||
208
docs/api/overview.md
Normal file
208
docs/api/overview.md
Normal file
@@ -0,0 +1,208 @@
|
||||
NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally.
|
||||
|
||||
# What is a REST API?
|
||||
|
||||
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create an object
|
||||
* `PUT` / `PATCH`: Modify an existing object. `PUT` requires all mandatory fields to be specified, while `PATCH` only expects the field that is being modified to be specified.
|
||||
* `DELETE`: Delete an existing object
|
||||
|
||||
The NetBox API represents all objects in [JavaScript Object Notation (JSON)](http://www.json.org/). This makes it very easy to interact with NetBox data on the command line with common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)
|
||||
|
||||
```
|
||||
$ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.'
|
||||
{
|
||||
"custom_fields": {},
|
||||
"nat_outside": null,
|
||||
"nat_inside": null,
|
||||
"description": "An example IP address",
|
||||
"id": 2954,
|
||||
"family": 4,
|
||||
"address": "5.101.108.132/26",
|
||||
"vrf": null,
|
||||
"tenant": null,
|
||||
"status": {
|
||||
"label": "Active",
|
||||
"value": 1
|
||||
},
|
||||
"role": null,
|
||||
"interface": null
|
||||
}
|
||||
```
|
||||
|
||||
Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.
|
||||
|
||||
# URL Hierarchy
|
||||
|
||||
NetBox's entire API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
|
||||
|
||||
* /api/circuits/providers/
|
||||
* /api/circuits/circuits/
|
||||
|
||||
Likewise, the site, rack, and device objects are located under the "DCIM" application:
|
||||
|
||||
* /api/dcim/sites/
|
||||
* /api/dcim/racks/
|
||||
* /api/dcim/devices/
|
||||
|
||||
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.
|
||||
|
||||
Each model generally has two views associated with it: a list view and a detail view. The list view is used to request a list of multiple objects or to create a new object. The detail view is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (`id`).
|
||||
|
||||
* /api/dcim/devices/ - List devices or create a new device
|
||||
* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123
|
||||
|
||||
Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
|
||||
|
||||
```
|
||||
GET /api/dcim/interfaces/?device_id=123
|
||||
```
|
||||
|
||||
# Serialization
|
||||
|
||||
The NetBox API employs three types of serializers to represent model data:
|
||||
|
||||
* Base serializer
|
||||
* Nested serializer
|
||||
* Writable serializer
|
||||
|
||||
The base serializer is used to represent the default view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
|
||||
|
||||
```
|
||||
{
|
||||
"id": 1048,
|
||||
"site": {
|
||||
"id": 7,
|
||||
"url": "http://localhost:8000/api/dcim/sites/7/",
|
||||
"name": "Corporate HQ",
|
||||
"slug": "corporate-hq"
|
||||
},
|
||||
"group": {
|
||||
"id": 4,
|
||||
"url": "http://localhost:8000/api/ipam/vlan-groups/4/",
|
||||
"name": "Production",
|
||||
"slug": "production"
|
||||
},
|
||||
"vid": 101,
|
||||
"name": "Users-Floor1",
|
||||
"tenant": null,
|
||||
"status": {
|
||||
"value": 1,
|
||||
"label": "Active"
|
||||
},
|
||||
"role": {
|
||||
"id": 9,
|
||||
"url": "http://localhost:8000/api/ipam/roles/9/",
|
||||
"name": "User Access",
|
||||
"slug": "user-access"
|
||||
},
|
||||
"description": "",
|
||||
"display_name": "101 (Users-Floor1)",
|
||||
"custom_fields": {}
|
||||
}
|
||||
```
|
||||
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||
|
||||
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
|
||||
|
||||
```
|
||||
{
|
||||
"id": 1201,
|
||||
"site": 7,
|
||||
"group": 4,
|
||||
"vid": 102,
|
||||
"name": "Users-Floor2",
|
||||
"tenant": null,
|
||||
"status": 1,
|
||||
"role": 9,
|
||||
"description": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Static Choice Fields
|
||||
|
||||
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
|
||||
|
||||
Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Container"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Reserved"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": "Deprecated"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`.
|
||||
|
||||
A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app.
|
||||
|
||||
# Pagination
|
||||
|
||||
API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes:
|
||||
|
||||
* `count`: The total count of all objects matching the query
|
||||
* `next`: A hyperlink to the next page of results (if applicable)
|
||||
* `previous`: A hyperlink to the previous page of results (if applicable)
|
||||
* `results`: The list of returned objects
|
||||
|
||||
Here is an example of a paginated response:
|
||||
|
||||
```
|
||||
HTTP 200 OK
|
||||
Allow: GET, POST, OPTIONS
|
||||
Content-Type: application/json
|
||||
Vary: Accept
|
||||
|
||||
{
|
||||
"count": 2861,
|
||||
"next": "http://localhost:8000/api/dcim/devices/?limit=50&offset=50",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 123,
|
||||
"name": "DeviceName123",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The default page size derives from the [`PAGINATE_COUNT`](../configuration/optional-settings/#paginate_count) configuration setting, 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://localhost:8000/api/dcim/devices/?limit=100
|
||||
```
|
||||
|
||||
The response will return devices 1 through 100. The URL provided in the `next` attribute of the response will return devices 101 through 200:
|
||||
|
||||
```
|
||||
{
|
||||
"count": 2861,
|
||||
"next": "http://localhost:8000/api/dcim/devices/?limit=100&offset=100",
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings/#max_page_size) setting, 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.
|
||||
136
docs/api/working-with-secrets.md
Normal file
136
docs/api/working-with-secrets.md
Normal file
@@ -0,0 +1,136 @@
|
||||
As with most other objects, the NetBox API can be used to create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data.
|
||||
|
||||
# Generating a Session Key
|
||||
|
||||
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../data-model/secrets/#user-keys). The private key must be POSTed with the name `private_key`.
|
||||
|
||||
```
|
||||
$ curl -X POST http://localhost:8000/api/secrets/get-session-key/ \
|
||||
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
--data-urlencode "private_key@<filename>"
|
||||
{
|
||||
"session_key": "dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
To read the private key from a file, use the convention above. Alternatively, the private key can be read from an environment variable using `--data-urlencode "private_key=$PRIVATE_KEY"`.
|
||||
|
||||
The request uses your private key to unlock your stored copy of the master key and generate a session key which can be attached in the `X-Session-Key` header of future API requests.
|
||||
|
||||
# Retrieving Secrets
|
||||
|
||||
A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its `plaintext` field set to null.
|
||||
|
||||
```
|
||||
$ curl http://localhost:8000/api/secrets/secrets/2587/ \
|
||||
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
|
||||
-H "Accept: application/json; indent=4"
|
||||
{
|
||||
"id": 2587,
|
||||
"device": {
|
||||
"id": 1827,
|
||||
"url": "http://localhost:8000/api/dcim/devices/1827/",
|
||||
"name": "MyTestDevice",
|
||||
"display_name": "MyTestDevice"
|
||||
},
|
||||
"role": {
|
||||
"id": 1,
|
||||
"url": "http://localhost:8000/api/secrets/secret-roles/1/",
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-creds"
|
||||
},
|
||||
"name": "admin",
|
||||
"plaintext": null,
|
||||
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
|
||||
"created": "2017-03-21",
|
||||
"last_updated": "2017-03-21T19:28:44.265582Z"
|
||||
}
|
||||
```
|
||||
|
||||
To decrypt a secret, we must include our session key in the `X-Session-Key` header:
|
||||
|
||||
```
|
||||
$ curl http://localhost:8000/api/secrets/secrets/2587/ \
|
||||
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
{
|
||||
"id": 2587,
|
||||
"device": {
|
||||
"id": 1827,
|
||||
"url": "http://localhost:8000/api/dcim/devices/1827/",
|
||||
"name": "MyTestDevice",
|
||||
"display_name": "MyTestDevice"
|
||||
},
|
||||
"role": {
|
||||
"id": 1,
|
||||
"url": "http://localhost:8000/api/secrets/secret-roles/1/",
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-creds"
|
||||
},
|
||||
"name": "admin",
|
||||
"plaintext": "foobar",
|
||||
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
|
||||
"created": "2017-03-21",
|
||||
"last_updated": "2017-03-21T19:28:44.265582Z"
|
||||
}
|
||||
```
|
||||
|
||||
Lists of secrets can be decrypted in this manner as well:
|
||||
|
||||
```
|
||||
$ curl http://localhost:8000/api/secrets/secrets/?limit=3 \
|
||||
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
|
||||
{
|
||||
"count": 3482,
|
||||
"next": "http://localhost:8000/api/secrets/secrets/?limit=3&offset=3",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 2587,
|
||||
...
|
||||
"plaintext": "foobar",
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 2588,
|
||||
...
|
||||
"plaintext": "MyP@ssw0rd!",
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 2589,
|
||||
...
|
||||
"plaintext": "AnotherSecret!",
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# Creating Secrets
|
||||
|
||||
Session keys are also used to decrypt new or modified secrets. This is done by setting the `plaintext` field of the submitted object:
|
||||
|
||||
```
|
||||
$ curl -X POST http://localhost:8000/api/secrets/secrets/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk=" \
|
||||
--data '{"device": 1827, "role": 1, "name": "backup", "plaintext": "Drowssap1"}'
|
||||
{
|
||||
"id": 2590,
|
||||
"device": 1827,
|
||||
"role": 1,
|
||||
"name": "backup",
|
||||
"plaintext": "Drowssap1"
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Don't forget to include the `Content-Type: application/json` header when making a POST request.
|
||||
@@ -17,7 +17,7 @@ ADMINS = [
|
||||
|
||||
## BANNER_BOTTOM
|
||||
|
||||
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
|
||||
Setting these variables will display 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:
|
||||
|
||||
```
|
||||
BANNER_TOP = 'Your banner text'
|
||||
@@ -26,6 +26,12 @@ BANNER_BOTTOM = BANNER_TOP
|
||||
|
||||
---
|
||||
|
||||
## BANNER_LOGIN
|
||||
|
||||
The value of this variable will be displayed on the login page above the login form. HTML is allowed.
|
||||
|
||||
---
|
||||
|
||||
## BASE_PATH
|
||||
|
||||
Default: None
|
||||
@@ -38,6 +44,22 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
||||
Default: False
|
||||
|
||||
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_WHITELIST
|
||||
|
||||
## CORS_ORIGIN_REGEX_WHITELIST
|
||||
|
||||
These settings specify a list of origins that are authorized to make cross-site API requests. Use `CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.)
|
||||
|
||||
---
|
||||
|
||||
## DEBUG
|
||||
|
||||
Default: False
|
||||
@@ -67,6 +89,34 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
|
||||
|
||||
---
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
|
||||
|
||||
```
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/netbox.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_REQUIRED
|
||||
|
||||
Default: False
|
||||
@@ -83,11 +133,61 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
|
||||
|
||||
---
|
||||
|
||||
## NETBOX_USERNAME
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
## NETBOX_PASSWORD
|
||||
Default: 1000
|
||||
|
||||
If provided, NetBox will use these credentials to authenticate against devices when collecting data.
|
||||
An API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This setting defines the maximum limit. Setting it to `0` or `None` will allow an API consumer to request all objects by specifying `?limit=0`.
|
||||
|
||||
---
|
||||
|
||||
## MEDIA_ROOT
|
||||
|
||||
Default: $BASE_DIR/netbox/media/
|
||||
|
||||
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path.
|
||||
|
||||
---
|
||||
|
||||
## NAPALM_USERNAME
|
||||
|
||||
## NAPALM_PASSWORD
|
||||
|
||||
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
|
||||
|
||||
Note: If SSH public key authentication has been set up 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](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
|
||||
|
||||
```
|
||||
NAPALM_ARGS = {
|
||||
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
|
||||
'port': 2222,
|
||||
}
|
||||
```
|
||||
|
||||
Note: 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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -107,6 +207,14 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
|
||||
|
||||
---
|
||||
|
||||
## REPORTS_ROOT
|
||||
|
||||
Default: $BASE_DIR/netbox/reports/
|
||||
|
||||
The file path to the location where custom reports will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
@@ -2,7 +2,7 @@ The circuits component of NetBox deals with the management of long-haul Internet
|
||||
|
||||
# Providers
|
||||
|
||||
A provider is any entity which provides some form of connectivity. This obviously includes carriers which offer Internet and private transit service. However, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||
A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||
|
||||
Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
|
||||
|
||||
@@ -14,7 +14,7 @@ A circuit represents a single physical data link connecting two endpoints. Each
|
||||
|
||||
### Circuit Types
|
||||
|
||||
Circuits are classified by type. For example:
|
||||
Circuits are classified by type. For example, you might define circuit types for:
|
||||
|
||||
* Internet transit
|
||||
* Out-of-band connectivity
|
||||
@@ -27,7 +27,7 @@ Circuit types are fully customizable.
|
||||
|
||||
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||
|
||||
Each circuit termination can be tied to a site, or to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
|
||||
!!! note
|
||||
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.
|
||||
|
||||
@@ -2,61 +2,72 @@ Data center infrastructure management (DCIM) entails all physical assets: sites,
|
||||
|
||||
# Sites
|
||||
|
||||
How you define sites will depend on the nature of your organization, but typically a site will equate a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
||||
How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
||||
|
||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
|
||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment, and an Autonomous System (AS) number.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
# Racks
|
||||
|
||||
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units* (U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack is assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U, but NetBox allows you to define racks of arbitrary height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||
|
||||
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, M204.313) whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
|
||||
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
|
||||
|
||||
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
||||
|
||||
### Rack Groups
|
||||
|
||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
|
||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
|
||||
|
||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
||||
|
||||
### Rack Roles
|
||||
|
||||
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
||||
Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.
|
||||
|
||||
### Rack Space Reservations
|
||||
|
||||
Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks).
|
||||
|
||||
---
|
||||
|
||||
# Device Types
|
||||
|
||||
A device type represents a particular manufacturer and model of equipment. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||
A device type represents a particular hardware model that exists in the real world. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||
|
||||
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type.
|
||||
|
||||
### Manufacturers
|
||||
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. Manufacturers are used to group different models of device.
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.
|
||||
|
||||
### Component Templates
|
||||
|
||||
Each device type is assigned a number of component templates which describe the console, power, and data ports a device has. These are:
|
||||
Each device type is assigned a number of component templates which define the physical interfaces a device has. These are:
|
||||
|
||||
* Console port templates
|
||||
* Console server port templates
|
||||
* Power port templates
|
||||
* Power outlet templates
|
||||
* Interface templates
|
||||
* Device bay templates
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Whenever a new device is created, it is automatically assigned console, power, and interface components per the templates assigned to its device type. For example, suppose your network employs Juniper EX4300-48T switches. You would create a device type with a model name "EX4300-48T" and assign it to the manufacturer "Juniper." You might then also create the following templates for it:
|
||||
Whenever a new device is created, it is automatically assigned components per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates:
|
||||
|
||||
* One template for a console port ("Console")
|
||||
* Two templates for power ports ("PSU0" and "PSU1")
|
||||
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
||||
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
||||
|
||||
Once you've done this, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
|
||||
Note that assignment of components from templates occurs only at the time of device creation: If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||
!!! note
|
||||
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||
|
||||
---
|
||||
|
||||
@@ -64,23 +75,26 @@ Note that assignment of components from templates occurs only at the time of dev
|
||||
|
||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
||||
|
||||
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8.
|
||||
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8. This logic applies to racks with both ascending and descending unit numbering.
|
||||
|
||||
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
||||
|
||||
### Roles
|
||||
|
||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.
|
||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, a device can belong to only one role.
|
||||
|
||||
### Platforms
|
||||
|
||||
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The assignment of platforms to devices is an entirely optional feature, and may be disregarded if not desired.
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
### Modules
|
||||
### Inventory Items
|
||||
|
||||
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each module can optionally be assigned to a manufacturer.
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each item can optionally be assigned a manufacturer.
|
||||
|
||||
!!! note
|
||||
Prior to version 2.0, inventory items were called modules.
|
||||
|
||||
### Components
|
||||
|
||||
@@ -93,10 +107,16 @@ There are six types of device components which comprise all of the interconnecti
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.)
|
||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.) Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
|
||||
|
||||
Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed. In addition to a connecting peer, interfaces are also assigned a form factor and may be designated as management-only (for out-of-band management). Interfaces may also be assigned a short description.
|
||||
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
|
||||
|
||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||
|
||||
Note that child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply.
|
||||
---
|
||||
|
||||
# Virtual Chassis
|
||||
|
||||
A virtual chassis represents a set of devices which share a single control plane: for example, a stack of switches which are managed as a single device. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
|
||||
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
|
||||
|
||||
@@ -2,7 +2,7 @@ This section entails features of NetBox which are not crucial to its primary fun
|
||||
|
||||
# Custom Fields
|
||||
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address` and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
||||
|
||||
@@ -33,7 +33,15 @@ NetBox allows users to define custom templates that can be used when exporting o
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
|
||||
|
||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop.
|
||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
|
||||
|
||||
```
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
@@ -44,10 +52,10 @@ A MIME type and file extension can optionally be defined for each export templat
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||
|
||||
```
|
||||
{% for d in queryset %}{% if d.status and d.primary_ip %}define host{
|
||||
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
|
||||
use generic-switch
|
||||
host_name {{ d.name }}
|
||||
address {{ d.primary_ip.address.ip }}
|
||||
host_name {{ device.name }}
|
||||
address {{ device.primary_ip.address.ip }}
|
||||
}
|
||||
{% endif %}{% endfor %}
|
||||
```
|
||||
@@ -74,19 +82,35 @@ define host{
|
||||
|
||||
# Graphs
|
||||
|
||||
NetBox does not generate graphs itself. This feature allows you to embed contextual graphs from an external resources inside certain NetBox views. Each embedded graph must be defined with the following parameters:
|
||||
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
||||
|
||||
* **Type:** Interface, provider, or site. This determines where the graph will be displayed.
|
||||
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
|
||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
||||
* **Name:** The title to display above the graph.
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||
|
||||
## Examples
|
||||
|
||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||
```
|
||||
|
||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
```
|
||||
|
||||
# Topology Maps
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend connectivity).
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
|
||||
|
||||
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
||||
|
||||
@@ -95,7 +119,14 @@ Each line of the **device patterns** field represents a hierarchical layer withi
|
||||
```
|
||||
core-switch-[abcd]
|
||||
dist-switch\d
|
||||
access-switch\d+,oob-switch\d+
|
||||
access-switch\d+;oob-switch\d+
|
||||
```
|
||||
|
||||
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
|
||||
# Image Attachments
|
||||
|
||||
Certain objects within NetBox (namely sites, racks, and devices) can have photos or other images attached to them. (Note that _only_ image files are supported.) Each attachment may optionally be assigned a name; if omitted, the attachment will be represented by its file name.
|
||||
|
||||
!!! note
|
||||
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).
|
||||
|
||||
@@ -6,11 +6,14 @@ A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain
|
||||
|
||||
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
!!! note
|
||||
By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
|
||||
|
||||
---
|
||||
|
||||
# Aggregates
|
||||
|
||||
IPv4 address space is organized as a hierarchy, with more-specific (smaller) prefix arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 10.1.0.0/16
|
||||
@@ -18,23 +21,23 @@ IPv4 address space is organized as a hierarchy, with more-specific (smaller) pre
|
||||
|
||||
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
|
||||
|
||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the RFC 1918 private IPv4 space. So, you might define three aggregates for this space:
|
||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 172.16.0.0/12
|
||||
* 192.168.0.0/16
|
||||
|
||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space.
|
||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
|
||||
|
||||
Any prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation.
|
||||
Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
|
||||
|
||||
Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix.
|
||||
Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8.
|
||||
|
||||
### RIRs
|
||||
|
||||
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
|
||||
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own).
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
|
||||
|
||||
---
|
||||
|
||||
@@ -44,7 +47,7 @@ A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 19
|
||||
|
||||
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
|
||||
|
||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. This can be helpful is replicating real-world IP assignments. Each prefix may also be assigned a short description.
|
||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
|
||||
|
||||
### Statuses
|
||||
|
||||
@@ -52,7 +55,7 @@ Each prefix is assigned an operational status. This is one of the following:
|
||||
|
||||
* Container - A summary of child prefixes
|
||||
* Active - Provisioned and in use
|
||||
* Reserved - Earmarked for future use
|
||||
* Reserved - Designated for future use
|
||||
* Deprecated - No longer in use
|
||||
|
||||
### Roles
|
||||
@@ -65,30 +68,32 @@ Whereas a status describes a prefix's operational state, a role describes its fu
|
||||
* Lab
|
||||
* Out-of-band
|
||||
|
||||
Role assignment is optional and you are free to create as many as you'd like.
|
||||
Role assignment is optional and roles are fully customizable.
|
||||
|
||||
---
|
||||
|
||||
# IP Addresses
|
||||
|
||||
An IP address comprises a single address (either IPv4 or IPv6) and its mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||
An IP address comprises a single address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||
|
||||
Like prefixes, an IP address can optionally be assigned to a VRF (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
|
||||
|
||||
Each IP address can optionally be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address.
|
||||
An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
|
||||
|
||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily is denoting the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not currently supported.
|
||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
|
||||
|
||||
---
|
||||
|
||||
# VLANs
|
||||
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
|
||||
|
||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
||||
### VLAN Groups
|
||||
|
||||
VLAN groups can be employed for administrative organization within NetBox. Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs, including within a site.
|
||||
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, SSH (TCP/22). A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any IP address.)
|
||||
A service represents a TCP or UDP service available on a device or virtual machine. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to its parent. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
|
||||
|
||||
@@ -24,11 +24,11 @@ Roles are also used to control access to secrets. Each role is assigned an arbit
|
||||
|
||||
Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
|
||||
|
||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already has access to retrieve secret data.
|
||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
|
||||
|
||||
## Creating the First User Key
|
||||
|
||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the super user) must create a user key. This can be done by navigating to Profile > User Key.
|
||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.
|
||||
|
||||
To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
|
||||
NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
|
||||
|
||||
# Tenants
|
||||
|
||||
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
|
||||
|
||||
The following objects can be assigned to tenants:
|
||||
A tenant represents a discrete organization. The following objects can be assigned to tenants:
|
||||
|
||||
* Sites
|
||||
* Racks
|
||||
|
||||
29
docs/data-model/virtualization.md
Normal file
29
docs/data-model/virtualization.md
Normal file
@@ -0,0 +1,29 @@
|
||||
NetBox supports the definition of virtual machines arranged in clusters. A cluster can optionally have physical host devices associated with it.
|
||||
|
||||
# Clusters
|
||||
|
||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned an organizational group.
|
||||
|
||||
Physical devices (from NetBox's DCIM component) may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||
|
||||
### Cluster Types
|
||||
|
||||
A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.
|
||||
|
||||
### Cluster Groups
|
||||
|
||||
Cluster groups may be created for the purpose of organizing clusters.
|
||||
|
||||
---
|
||||
|
||||
# Virtual Machines
|
||||
|
||||
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
|
||||
|
||||
Like devices, each VM can have interfaces created on it. These behave similarly to device interfaces, and can be assigned IP addresses, however given their virtual nature they cannot be connected to other interfaces. VMs can also be assigned layer four services. Unlike physical devices, VMs cannot be assigned console or power ports, or device bays.
|
||||
|
||||
The following resources can be defined for each VM:
|
||||
|
||||
* vCPU count
|
||||
* Memory (MB)
|
||||
* Disk space (GB)
|
||||
53
docs/development/utility-views.md
Normal file
53
docs/development/utility-views.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Utility Views
|
||||
|
||||
Utility views are reusable views that handle common CRUD tasks, such as listing and updating objects. Some views operate on individual objects, whereas others (referred to as "bulk" views) operate on multiple objects at once.
|
||||
|
||||
## Individual Views
|
||||
|
||||
### ObjectListView
|
||||
|
||||
Generates a paginated table of objects from a given queryset, which may optionally be filtered.
|
||||
|
||||
### ObjectEditView
|
||||
|
||||
Updates an object identified by a primary key (PK) or slug. If no existing object is specified, a new object will be created.
|
||||
|
||||
### ObjectDeleteView
|
||||
|
||||
Deletes an object. The user is redirected to a confirmation page before the deletion is executed.
|
||||
|
||||
## Bulk Views
|
||||
|
||||
### BulkCreateView
|
||||
|
||||
Creates multiple objects at once based on a given pattern. Currently used only for IP addresses.
|
||||
|
||||
### BulkImportView
|
||||
|
||||
Accepts CSV-formatted data and creates a new object for each line. Creation is all-or-none.
|
||||
|
||||
### BulkEditView
|
||||
|
||||
Applies changes to multiple objects at once in a two-step operation. First, the list of PKs for selected objects is POSTed and an edit form is presented to the user. On submission of that form, the specified changes are made to all selected objects.
|
||||
|
||||
### BulkDeleteView
|
||||
|
||||
Deletes multiple objects. The user selects the objects to be deleted and confirms the deletion.
|
||||
|
||||
## Component Views
|
||||
|
||||
### ComponentCreateView
|
||||
|
||||
Create one or more component objects beloning to a parent object (e.g. interfaces attached to a device).
|
||||
|
||||
### ComponentEditView
|
||||
|
||||
A subclass of `ObjectEditView`: Updates an individual component object.
|
||||
|
||||
### ComponentDeleteView
|
||||
|
||||
A subclass of `ObjectDeleteView`: Deletes an individual component object.
|
||||
|
||||
### BulkComponentCreateView
|
||||
|
||||
Create a set of components objects for each of a selected set of parent objects. This view can be used e.g. to create multiple interfaces on multiple devices at once.
|
||||
@@ -6,6 +6,7 @@ NetBox is an open source web application designed to help manage and document co
|
||||
* **Equipment racks** - Organized by group and site
|
||||
* **Devices** - Types of devices and where they are installed
|
||||
* **Connections** - Network, console, and power connections among devices
|
||||
* **Virtualization** - Virtual machines and clusters
|
||||
* **Data circuits** - Long-haul communications circuits and providers
|
||||
* **Secrets** - Encrypted storage of sensitive credentials
|
||||
|
||||
@@ -46,7 +47,7 @@ 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 |
|
||||
| Database | PostgreSQL 9.4+ |
|
||||
|
||||
# Getting Started
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
This guide demonstrates how to build and run NetBox as a Docker container. It assumes that the latest versions of [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are already installed in your host.
|
||||
|
||||
# Quickstart
|
||||
|
||||
To get NetBox up and running:
|
||||
|
||||
```no-highlight
|
||||
# git clone -b master https://github.com/digitalocean/netbox.git
|
||||
# cd netbox
|
||||
# docker-compose up -d
|
||||
```
|
||||
|
||||
The application will be available on http://localhost/ after a few minutes.
|
||||
|
||||
Default credentials:
|
||||
|
||||
* Username: **admin**
|
||||
* Password: **admin**
|
||||
|
||||
# Configuration
|
||||
|
||||
You can configure the app at runtime using variables (see `docker-compose.yml`). Possible environment variables include:
|
||||
|
||||
* SUPERUSER_NAME
|
||||
* SUPERUSER_EMAIL
|
||||
* SUPERUSER_PASSWORD
|
||||
* ALLOWED_HOSTS
|
||||
* DB_NAME
|
||||
* DB_USER
|
||||
* DB_PASSWORD
|
||||
* DB_HOST
|
||||
* DB_PORT
|
||||
* SECRET_KEY
|
||||
* EMAIL_SERVER
|
||||
* EMAIL_PORT
|
||||
* EMAIL_USERNAME
|
||||
* EMAIL_PASSWORD
|
||||
* EMAIL_TIMEOUT
|
||||
* EMAIL_FROM
|
||||
* LOGIN_REQUIRED
|
||||
* MAINTENANCE_MODE
|
||||
* NETBOX_USERNAME
|
||||
* NETBOX_PASSWORD
|
||||
* PAGINATE_COUNT
|
||||
* TIME_ZONE
|
||||
* DATE_FORMAT
|
||||
* SHORT_DATE_FORMAT
|
||||
* TIME_FORMAT
|
||||
* SHORT_TIME_FORMAT
|
||||
* DATETIME_FORMAT
|
||||
* SHORT_DATETIME_FORMAT
|
||||
@@ -1,5 +1,4 @@
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to
|
||||
built-in Django users in the event of a failure.
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to built-in Django users in the event of a failure.
|
||||
|
||||
# Requirements
|
||||
|
||||
@@ -25,10 +24,13 @@ sudo pip install django-auth-ldap
|
||||
|
||||
# Configuration
|
||||
|
||||
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`.
|
||||
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/).
|
||||
|
||||
## General Server Configuration
|
||||
|
||||
!!! info
|
||||
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
|
||||
|
||||
```python
|
||||
import ldap
|
||||
|
||||
@@ -50,8 +52,13 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
||||
LDAP_IGNORE_CERT_ERRORS = True
|
||||
```
|
||||
|
||||
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||
|
||||
## User Authentication
|
||||
|
||||
!!! info
|
||||
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
@@ -67,11 +74,14 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
|
||||
# You can map user attributes to Django attributes as so.
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"first_name": "givenName",
|
||||
"last_name": "sn"
|
||||
"last_name": "sn",
|
||||
"email": "mail"
|
||||
}
|
||||
```
|
||||
|
||||
# User Groups for Permissions
|
||||
!!! info
|
||||
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||
@@ -99,3 +109,7 @@ AUTH_LDAP_FIND_GROUP_PERMS = True
|
||||
AUTH_LDAP_CACHE_GROUPS = True
|
||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
```
|
||||
|
||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||
|
||||
33
docs/installation/migrating-to-python3.md
Normal file
33
docs/installation/migrating-to-python3.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Migration
|
||||
|
||||
Remove Python 2 packages
|
||||
|
||||
```no-highlight
|
||||
# apt-get remove --purge -y python-dev python-pip
|
||||
```
|
||||
|
||||
Install Python 3 packages
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip
|
||||
```
|
||||
|
||||
Install Python Packages
|
||||
|
||||
```no-highlight
|
||||
# cd /opt/netbox
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Gunicorn Update
|
||||
|
||||
```no-highlight
|
||||
# pip uninstall gunicorn
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Re-install LDAP Module (optional if using LDAP for auth)
|
||||
|
||||
```no-highlight
|
||||
sudo pip3 install django-auth-ldap
|
||||
```
|
||||
@@ -1,33 +1,42 @@
|
||||
# Installation
|
||||
|
||||
**Debian/Ubuntu**
|
||||
This section of the documentation discusses installing and configuring the NetBox application.
|
||||
|
||||
!!! note
|
||||
Python 3 is strongly encouraged for new installations. Support for Python 2 will be discontinued in the near future. This documentation includes a guide on [migrating from Python 2 to Python 3](migrating-to-python3).
|
||||
|
||||
**Ubuntu**
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
# easy_install3 pip
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||
# apt-get install -y python2.7 python-dev python-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
# easy_install pip
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
# easy_install-3.4 pip
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# yum install -y gcc python2 python-devel python-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
# easy_install pip
|
||||
```
|
||||
|
||||
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
||||
@@ -54,13 +63,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
|
||||
|
||||
If `git` is not already installed, install it:
|
||||
|
||||
**Debian/Ubuntu**
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y git
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y git
|
||||
@@ -79,14 +88,40 @@ Resolving deltas: 100% (1495/1495), done.
|
||||
Checking connectivity... done.
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.)
|
||||
|
||||
```
|
||||
# chown -R netbox:netbox /opt/netbox/netbox/media/
|
||||
```
|
||||
|
||||
## Install Python Packages
|
||||
|
||||
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# pip install -r requirements.txt
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip -V` or `pip3 -V`.
|
||||
|
||||
### NAPALM Automation
|
||||
|
||||
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
|
||||
|
||||
```no-highlight
|
||||
# pip3 install napalm
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
||||
@@ -139,11 +174,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
|
||||
|
||||
# Run Database Migrations
|
||||
|
||||
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||
!!! warning
|
||||
The examples on the rest of this page call the `python3` executable. Replace this with `python2` or `python` if you're using Python 2.
|
||||
|
||||
Before NetBox can run, we need to install the database schema. This is done by running `python3 manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||
|
||||
```no-highlight
|
||||
# cd /opt/netbox/netbox/
|
||||
# ./manage.py migrate
|
||||
# python3 manage.py migrate
|
||||
Operations to perform:
|
||||
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
||||
Running migrations:
|
||||
@@ -161,7 +199,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
|
||||
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py createsuperuser
|
||||
# python3 manage.py createsuperuser
|
||||
Username: admin
|
||||
Email address: admin@example.com
|
||||
Password:
|
||||
@@ -172,7 +210,7 @@ Superuser created successfully.
|
||||
# Collect Static Files
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py collectstatic
|
||||
# python3 manage.py collectstatic --no-input
|
||||
|
||||
You have requested to collect static files at the destination
|
||||
location as specified in your settings:
|
||||
@@ -193,7 +231,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
|
||||
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py loaddata initial_data
|
||||
# python3 manage.py loaddata initial_data
|
||||
Installed 43 object(s) from 4 fixture(s)
|
||||
```
|
||||
|
||||
@@ -202,7 +240,7 @@ Installed 43 object(s) from 4 fixture(s)
|
||||
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py runserver 0.0.0.0:8000 --insecure
|
||||
# python3 manage.py runserver 0.0.0.0:8000 --insecure
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
|
||||
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).)
|
||||
|
||||
!!! note
|
||||
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. 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.
|
||||
|
||||
!!! warning
|
||||
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
|
||||
|
||||
# Installation
|
||||
|
||||
**Debian/Ubuntu**
|
||||
**Ubuntu**
|
||||
|
||||
If a recent enough version of PostgreSQL is not available through your distribution's package manager, you'll need to install it from an official [PostgreSQL repository](https://wiki.postgresql.org/wiki/Apt).
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y postgresql libpq-dev python-psycopg2
|
||||
# apt-get update
|
||||
# apt-get install -y postgresql libpq-dev
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
|
||||
|
||||
```no-highlight
|
||||
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
|
||||
# postgresql-setup initdb
|
||||
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
|
||||
# yum install postgresql96 postgresql96-server postgresql96-devel
|
||||
# /usr/pgsql-9.6/bin/postgresql96-setup initdb
|
||||
```
|
||||
|
||||
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
|
||||
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/9.6/data/pg_hba.conf`. For example:
|
||||
|
||||
```no-highlight
|
||||
host all all 127.0.0.1/32 md5
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
Then, start the service:
|
||||
Then, start the service and enable it to run at boot:
|
||||
|
||||
```no-highlight
|
||||
# systemctl start postgresql
|
||||
# systemctl start postgresql-9.6
|
||||
# systemctl enable postgresql-9.6
|
||||
```
|
||||
|
||||
# Database Creation
|
||||
@@ -37,7 +50,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
|
||||
|
||||
```no-highlight
|
||||
# sudo -u postgres psql
|
||||
psql (9.3.13)
|
||||
psql (9.4.5)
|
||||
Type "help" for help.
|
||||
|
||||
postgres=# CREATE DATABASE netbox;
|
||||
@@ -49,10 +62,10 @@ GRANT
|
||||
postgres=# \q
|
||||
```
|
||||
|
||||
You can verify that authentication works issuing the following command and providing the configured password:
|
||||
You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.)
|
||||
|
||||
```no-highlight
|
||||
# psql -U netbox -h localhost -W
|
||||
# psql -U netbox -W -h localhost netbox
|
||||
```
|
||||
|
||||
If successful, you will enter a `postgres` prompt. Type `\q` to exit.
|
||||
If successful, you will enter a `netbox` prompt. Type `\q` to exit.
|
||||
|
||||
@@ -27,6 +27,12 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||
```
|
||||
|
||||
Copy the LDAP configuration if using LDAP:
|
||||
|
||||
```no-highlight
|
||||
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||
```
|
||||
|
||||
## Option B: Clone the Git Repository (latest master release)
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||
@@ -46,12 +52,27 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
||||
# ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
|
||||
|
||||
```no-highlight
|
||||
# ./upgrade.sh -2
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
* Installs or upgrades any new required Python packages
|
||||
* Applies any database migrations that were included in the release
|
||||
* Collects all static files to be served by the HTTP service
|
||||
|
||||
!!! note
|
||||
It's possible that the upgrade script will display a notice warning of unreflected database migrations:
|
||||
|
||||
Your models have changes that are not yet reflected in a migration, and so won't be applied.
|
||||
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||
|
||||
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are intentionally modifying the database schema.
|
||||
|
||||
# Restart the WSGI Service
|
||||
|
||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
# Web Server Installation
|
||||
|
||||
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
|
||||
|
||||
!!! info
|
||||
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
|
||||
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y gunicorn supervisor
|
||||
```
|
||||
# Web Server Installation
|
||||
|
||||
## Option A: nginx
|
||||
|
||||
@@ -25,7 +21,7 @@ server {
|
||||
|
||||
server_name netbox.example.com;
|
||||
|
||||
access_log off;
|
||||
client_max_body_size 25m;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/netbox/netbox/static/;
|
||||
@@ -55,7 +51,7 @@ Restart the nginx service to use the new configuration.
|
||||
# service nginx restart
|
||||
```
|
||||
|
||||
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
|
||||
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04).
|
||||
|
||||
## Option B: Apache
|
||||
|
||||
@@ -73,6 +69,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
|
||||
|
||||
Alias /static /opt/netbox/netbox/static
|
||||
|
||||
# Needed to allow token-based API authentication
|
||||
WSGIPassAuthorization on
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
@@ -97,10 +96,16 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
|
||||
# service apache2 restart
|
||||
```
|
||||
|
||||
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04).
|
||||
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
|
||||
|
||||
# gunicorn Installation
|
||||
|
||||
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
|
||||
|
||||
```no-highlight
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
|
||||
```no-highlight
|
||||
@@ -113,6 +118,12 @@ user = 'www-data'
|
||||
|
||||
# supervisord Installation
|
||||
|
||||
Install supervisor:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y supervisor
|
||||
```
|
||||
|
||||
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
|
||||
```no-highlight
|
||||
|
||||
131
docs/miscellaneous/reports.md
Normal file
131
docs/miscellaneous/reports.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# NetBox Reports
|
||||
|
||||
A NetBox report is a mechanism for validating the integrity of data within NetBox. Running a report allows the user to verify that the objects defined within NetBox meet certain arbitrary conditions. For example, you can write reports to check that:
|
||||
|
||||
* All top-of-rack switches have a console connection
|
||||
* Every router has a loopback interface with an IP address assigned
|
||||
* Each interface description conforms to a standard format
|
||||
* Every site has a minimum set of VLANs defined
|
||||
* All IP addresses have a parent prefix
|
||||
|
||||
...and so on. Reports are completely customizable, so there's practically no limit to what you can test for.
|
||||
|
||||
## Writing Reports
|
||||
|
||||
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
|
||||
|
||||
!!! warning
|
||||
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
|
||||
|
||||
For example, we can create a module named `devices.py` to hold all of our reports which pertain to devices in NetBox. Within that module, we might define several reports. Each report is defined as a Python class inheriting from `extras.reports.Report`.
|
||||
|
||||
```
|
||||
from extras.reports import Report
|
||||
|
||||
class DeviceConnectionsReport(Report):
|
||||
description = "Validate the minimum physical connections for each device"
|
||||
|
||||
class DeviceIPsReport(Report):
|
||||
description = "Check that every device has a primary IP address assigned"
|
||||
```
|
||||
|
||||
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
|
||||
|
||||
```
|
||||
from dcim.constants import CONNECTION_STATUS_PLANNED, STATUS_ACTIVE
|
||||
from dcim.models import ConsolePort, Device, PowerPort
|
||||
from extras.reports import Report
|
||||
|
||||
|
||||
class DeviceConnectionsReport(Report):
|
||||
description = "Validate the minimum physical connections for each device"
|
||||
|
||||
def test_console_connection(self):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
for console_port in ConsolePort.objects.select_related('device').filter(device__status=STATUS_ACTIVE):
|
||||
if console_port.cs_port is None:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
"No console connection defined for {}".format(console_port.name)
|
||||
)
|
||||
elif console_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
self.log_warning(
|
||||
console_port.device,
|
||||
"Console connection for {} marked as planned".format(console_port.name)
|
||||
)
|
||||
else:
|
||||
self.log_success(console_port.device)
|
||||
|
||||
def test_power_connections(self):
|
||||
|
||||
# Check that every active device has at least two connected power supplies.
|
||||
for device in Device.objects.filter(status=STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.power_outlet is not None:
|
||||
connected_ports += 1
|
||||
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
self.log_warning(
|
||||
device,
|
||||
"Power connection for {} marked as planned".format(power_port.name)
|
||||
)
|
||||
if connected_ports < 2:
|
||||
self.log_failure(
|
||||
device,
|
||||
"{} connected power supplies found (2 needed)".format(connected_ports)
|
||||
)
|
||||
else:
|
||||
self.log_success(device)
|
||||
```
|
||||
|
||||
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed.
|
||||
|
||||
!!! warning
|
||||
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
|
||||
|
||||
The following methods are available to log results within a report:
|
||||
|
||||
* log(message)
|
||||
* log_success(object, message=None)
|
||||
* log_info(object, message)
|
||||
* log_warning(object, message)
|
||||
* log_failure(object, message)
|
||||
|
||||
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status.
|
||||
|
||||
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.
|
||||
|
||||
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
|
||||
|
||||
## Running Reports
|
||||
|
||||
### Via the Web UI
|
||||
|
||||
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Note that a user must have permission to create ReportResults in order to run reports. (Permissions can be assigned through the admin UI.)
|
||||
|
||||
Once a report has been run, its associated results will be included in the report view.
|
||||
|
||||
### Via the API
|
||||
|
||||
To run a report via the API, simply issue a POST request to its `run` endpoint. Reports are identified by their module and class name.
|
||||
|
||||
```
|
||||
POST /api/extras/reports/<module>.<name>/run/
|
||||
```
|
||||
|
||||
Our example report above would be called as:
|
||||
|
||||
```
|
||||
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
|
||||
```
|
||||
|
||||
### Via the CLI
|
||||
|
||||
Reports can be run on the CLI by invoking the management command:
|
||||
|
||||
```
|
||||
python3 manage.py runreport <module>
|
||||
```
|
||||
|
||||
One or more report modules may be specified.
|
||||
194
docs/miscellaneous/shell.md
Normal file
194
docs/miscellaneous/shell.md
Normal file
@@ -0,0 +1,194 @@
|
||||
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||
|
||||
```
|
||||
./manage.py nbshell
|
||||
```
|
||||
|
||||
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||
|
||||
```
|
||||
$ ./manage.py nbshell
|
||||
### NetBox interactive shell (jstretch-laptop)
|
||||
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||
```
|
||||
|
||||
The function `lsmodels()` will print a list of all available NetBox models:
|
||||
|
||||
```
|
||||
>>> lsmodels()
|
||||
DCIM:
|
||||
ConsolePort
|
||||
ConsolePortTemplate
|
||||
ConsoleServerPort
|
||||
ConsoleServerPortTemplate
|
||||
Device
|
||||
...
|
||||
```
|
||||
|
||||
## Querying Objects
|
||||
|
||||
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
|
||||
|
||||
```
|
||||
>>> Device.objects.all()
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice4>, <Device: TestDevice5>, '...(remaining elements truncated)...']>
|
||||
```
|
||||
|
||||
Use a `for` loop to cycle through all objects in the list:
|
||||
|
||||
```
|
||||
>>> for device in Device.objects.all():
|
||||
... print(device.name, device.device_type)
|
||||
...
|
||||
(u'TestDevice1', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice2', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice3', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice4', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice5', <DeviceType: PacketThingy 9000>)
|
||||
...
|
||||
```
|
||||
|
||||
To count all objects matching the query, replace `all()` with `count()`:
|
||||
|
||||
```
|
||||
>>> Device.objects.count()
|
||||
1274
|
||||
```
|
||||
|
||||
To retrieve a particular object (typically by its primary key or other unique field), use `get()`:
|
||||
|
||||
```
|
||||
>>> Site.objects.get(pk=7)
|
||||
<Site: Test Lab>
|
||||
```
|
||||
|
||||
### Filtering Querysets
|
||||
|
||||
In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example:
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE)
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice8>, <Device: TestDevice9>, '...(remaining elements truncated)...']>
|
||||
```
|
||||
|
||||
Querysets support slicing to return a specific range of objects.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE)[:3]
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>]>
|
||||
```
|
||||
|
||||
The `count()` method can be appended to the queryset to return a count of objects rather than the full list.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||
982
|
||||
```
|
||||
|
||||
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(tenant__name='Pied Piper')
|
||||
```
|
||||
|
||||
This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America:
|
||||
|
||||
```
|
||||
>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america')
|
||||
```
|
||||
|
||||
!!! note
|
||||
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
|
||||
|
||||
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(interfaces__name='em0')
|
||||
```
|
||||
|
||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(name__icontains='testdevice')
|
||||
```
|
||||
|
||||
Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value.
|
||||
|
||||
```
|
||||
>>> VLAN.objects.filter(vid__gt=2000)
|
||||
```
|
||||
|
||||
Multiple filters can be combined to further refine a queryset.
|
||||
|
||||
```
|
||||
>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering')
|
||||
```
|
||||
|
||||
To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`.
|
||||
|
||||
```
|
||||
>>> Device.objects.count()
|
||||
4479
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||
4133
|
||||
>>> Device.objects.exclude(status=STATUS_ACTIVE).count()
|
||||
346
|
||||
```
|
||||
|
||||
!!! info
|
||||
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
|
||||
|
||||
## Creating and Updating Objects
|
||||
|
||||
New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance.
|
||||
|
||||
```
|
||||
>>> lab1 = Site.objects.get(pk=7)
|
||||
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
||||
>>> myvlan.save()
|
||||
```
|
||||
|
||||
Alternatively, the above can be performed as a single operation:
|
||||
|
||||
```
|
||||
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
|
||||
```
|
||||
|
||||
To modify an object, retrieve it, update the desired field(s), and call `save()` again.
|
||||
|
||||
```
|
||||
>>> vlan = VLAN.objects.get(pk=1280)
|
||||
>>> vlan.name
|
||||
u'MyNewVLAN'
|
||||
>>> vlan.name = 'BetterName'
|
||||
>>> vlan.save()
|
||||
>>> VLAN.objects.get(pk=1280).name
|
||||
u'BetterName'
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.
|
||||
|
||||
## Deleting Objects
|
||||
|
||||
To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation.
|
||||
|
||||
```
|
||||
>>> vlan
|
||||
<VLAN: 123 (BetterName)>
|
||||
>>> vlan.delete()
|
||||
(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1})
|
||||
```
|
||||
|
||||
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(name__icontains='test').count()
|
||||
27
|
||||
>>> Device.objects.filter(name__icontains='test').delete()
|
||||
(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0})
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset.
|
||||
14
mkdocs.yml
14
mkdocs.yml
@@ -8,7 +8,7 @@ pages:
|
||||
- 'Web Server': 'installation/web-server.md'
|
||||
- 'LDAP (Optional)': 'installation/ldap.md'
|
||||
- 'Upgrading': 'installation/upgrading.md'
|
||||
- 'Alternate Install: Docker': 'installation/docker.md'
|
||||
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
|
||||
- 'Configuration':
|
||||
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
||||
- 'Optional Settings': 'configuration/optional-settings.md'
|
||||
@@ -18,8 +18,18 @@ pages:
|
||||
- 'IPAM': 'data-model/ipam.md'
|
||||
- 'Secrets': 'data-model/secrets.md'
|
||||
- 'Tenancy': 'data-model/tenancy.md'
|
||||
- 'Virtualization': 'data-model/virtualization.md'
|
||||
- 'Extras': 'data-model/extras.md'
|
||||
- 'API Integration': 'api-integration.md'
|
||||
- 'API':
|
||||
- 'Overview': 'api/overview.md'
|
||||
- 'Authentication': 'api/authentication.md'
|
||||
- 'Working with Secrets': 'api/working-with-secrets.md'
|
||||
- 'Examples': 'api/examples.md'
|
||||
- 'Miscellaneous':
|
||||
- 'Reports': 'miscellaneous/reports.md'
|
||||
- 'Shell': 'miscellaneous/shell.md'
|
||||
- 'Development':
|
||||
- 'Utility Views': 'development/utility-views.md'
|
||||
|
||||
markdown_extensions:
|
||||
- admonition:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Provider, CircuitType, Circuit
|
||||
|
||||
|
||||
@admin.register(Provider)
|
||||
class ProviderAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'asn']
|
||||
|
||||
|
||||
@admin.register(CircuitType)
|
||||
class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
|
||||
list_filter = ['provider', 'type', 'tenant']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||
return qs.select_related('provider', 'type', 'tenant')
|
||||
@@ -1,72 +1,122 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from extras.api.serializers import CustomFieldSerializer
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
class ProviderSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'custom_fields']
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class ProviderNestedSerializer(ProviderSerializer):
|
||||
class NestedProviderSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
|
||||
class Meta(ProviderSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableProviderSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(serializers.ModelSerializer):
|
||||
class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
||||
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
class Meta(CircuitTypeSerializer.Meta):
|
||||
pass
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info']
|
||||
|
||||
|
||||
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
terminations = CircuitTerminationSerializer(many=True)
|
||||
class CircuitSerializer(CustomFieldModelSerializer):
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
'terminations', 'custom_fields']
|
||||
fields = [
|
||||
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
'comments', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
class CircuitNestedSerializer(CircuitSerializer):
|
||||
class NestedCircuitSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta(CircuitSerializer.Meta):
|
||||
fields = ['id', 'cid']
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'cid']
|
||||
|
||||
|
||||
class WritableCircuitSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
'comments', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
interface = InterfaceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
]
|
||||
|
||||
|
||||
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
]
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
from django.conf.urls import url
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from extras.models import GRAPH_TYPE_PROVIDER
|
||||
from extras.api.views import GraphListView
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import *
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
class CircuitsRootView(routers.APIRootView):
|
||||
"""
|
||||
Circuits API root view
|
||||
"""
|
||||
def get_view_name(self):
|
||||
return 'Circuits'
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/(?P<pk>\d+)/$', ProviderDetailView.as_view(), name='provider_detail'),
|
||||
url(r'^providers/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER},
|
||||
name='provider_graphs'),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'),
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = CircuitsRootView
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'),
|
||||
# Field choices
|
||||
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, base_name='field-choice')
|
||||
|
||||
]
|
||||
# Providers
|
||||
router.register(r'providers', views.ProviderViewSet)
|
||||
|
||||
# Circuits
|
||||
router.register(r'circuit-types', views.CircuitTypeViewSet)
|
||||
router.register(r'circuits', views.CircuitViewSet)
|
||||
router.register(r'circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,58 +1,76 @@
|
||||
from rest_framework import generics
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from circuits.filters import CircuitFilter
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
|
||||
from extras.api.views import CustomFieldModelAPIView
|
||||
from circuits import filters
|
||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
|
||||
class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List all providers
|
||||
"""
|
||||
queryset = Provider.objects.prefetch_related('custom_field_values__field')
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(CircuitTermination, ['term_side']),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderViewSet(CustomFieldModelViewSet):
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
write_serializer_class = serializers.WritableProviderSerializer
|
||||
filter_class = filters.ProviderFilter
|
||||
|
||||
@detail_route()
|
||||
def graphs(self, request, pk=None):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular provider.
|
||||
"""
|
||||
provider = get_object_or_404(Provider, pk=pk)
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single provider
|
||||
"""
|
||||
queryset = Provider.objects.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
#
|
||||
# Circuit Types
|
||||
#
|
||||
|
||||
|
||||
class CircuitTypeListView(generics.ListAPIView):
|
||||
"""
|
||||
List all circuit types
|
||||
"""
|
||||
class CircuitTypeViewSet(ModelViewSet):
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filter_class = filters.CircuitTypeFilter
|
||||
|
||||
|
||||
class CircuitTypeDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit type
|
||||
"""
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
|
||||
class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List circuits (filterable)
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filter_class = CircuitFilter
|
||||
write_serializer_class = serializers.WritableCircuitSerializer
|
||||
filter_class = filters.CircuitFilter
|
||||
|
||||
|
||||
class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
#
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(ModelViewSet):
|
||||
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
write_serializer_class = serializers.WritableCircuitTerminationSerializer
|
||||
filter_class = filters.CircuitTerminationFilter
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
26
netbox/circuits/constants.py
Normal file
26
netbox/circuits/constants.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
# Circuit statuses
|
||||
CIRCUIT_STATUS_DEPROVISIONING = 0
|
||||
CIRCUIT_STATUS_ACTIVE = 1
|
||||
CIRCUIT_STATUS_PLANNED = 2
|
||||
CIRCUIT_STATUS_PROVISIONING = 3
|
||||
CIRCUIT_STATUS_OFFLINE = 4
|
||||
CIRCUIT_STATUS_DECOMMISSIONED = 5
|
||||
CIRCUIT_STATUS_CHOICES = [
|
||||
[CIRCUIT_STATUS_PLANNED, 'Planned'],
|
||||
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
|
||||
[CIRCUIT_STATUS_ACTIVE, 'Active'],
|
||||
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
|
||||
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
|
||||
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
|
||||
]
|
||||
|
||||
# CircuitTermination sides
|
||||
TERM_SIDE_A = 'A'
|
||||
TERM_SIDE_Z = 'Z'
|
||||
TERM_SIDE_CHOICES = (
|
||||
(TERM_SIDE_A, 'A'),
|
||||
(TERM_SIDE_Z, 'Z'),
|
||||
)
|
||||
@@ -1,18 +1,20 @@
|
||||
import django_filters
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
from utilities.filters import NumericInFilter
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -29,23 +31,34 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'account', 'asn']
|
||||
fields = ['name', 'slug', 'asn', 'account']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(noc_contact__icontains=value) |
|
||||
Q(admin_contact__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
)
|
||||
@@ -56,7 +69,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Provider (slug)',
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
queryset=CircuitType.objects.all(),
|
||||
label='Circuit type (ID)',
|
||||
)
|
||||
@@ -66,13 +78,16 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -91,9 +106,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['install_date']
|
||||
fields = ['cid', 'install_date', 'commit_rate']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(terminations__xconnect_id__icontains=value) |
|
||||
@@ -101,3 +118,37 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
label='Circuit',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(xconnect_id__icontains=value) |
|
||||
Q(pp_info__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from dcim.models import Site, Device, Interface, Rack
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||
SlugField,
|
||||
APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
|
||||
CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
@@ -36,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderFromCSVForm(forms.ModelForm):
|
||||
class ProviderCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
fields = Provider.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Provider name',
|
||||
'asn': '32-bit autonomous system number',
|
||||
'portal_url': 'Portal URL',
|
||||
'comments': 'Free-form comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -64,6 +70,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Provider
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
|
||||
|
||||
#
|
||||
@@ -78,16 +85,30 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = CircuitType.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
@@ -95,27 +116,50 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Provider not found.'})
|
||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
class CircuitCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent provider',
|
||||
error_messages={
|
||||
'invalid_choice': 'Provider not found.'
|
||||
}
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid circuit type.'
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
|
||||
|
||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), required=False, initial='')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
@@ -125,42 +169,95 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
|
||||
|
||||
|
||||
def circuit_status_choices():
|
||||
status_counts = {}
|
||||
for status in Circuit.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in CIRCUIT_STATUS_CHOICES]
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug')
|
||||
type = FilterChoiceField(
|
||||
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
provider = FilterChoiceField(
|
||||
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=circuit_status_choices, required=False)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
|
||||
|
||||
|
||||
#
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('rack', 'rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.connectable().select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
),
|
||||
chains=(
|
||||
('device', 'device'),
|
||||
),
|
||||
required=False,
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info']
|
||||
fields = [
|
||||
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
@@ -172,45 +269,22 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.interface is not None:
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
initial['rack'] = instance.interface.device.rack
|
||||
initial['device'] = instance.interface.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# If an interface has been assigned, initialize rack and device
|
||||
if self.instance.interface:
|
||||
self.initial['rack'] = self.instance.interface.device.rack
|
||||
self.initial['device'] = self.instance.interface.device
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||
elif self.initial.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
self.fields['interface'].choices = [
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||
}) for iface in interfaces
|
||||
]
|
||||
# Mark connected interfaces as disabled
|
||||
self.fields['interface'].choices = []
|
||||
for iface in self.fields['interface'].queryset:
|
||||
self.fields['interface'].choices.append(
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-19 17:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0007_circuit_add_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='interface',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'),
|
||||
),
|
||||
]
|
||||
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0008_circuittermination_interface_protect_on_delete'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='cid',
|
||||
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='commit_rate',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='install_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='port_speed',
|
||||
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='pp_info',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='term_side',
|
||||
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='upstream_speed',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='xconnect_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
field=models.TextField(blank=True, verbose_name='Admin contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
field=models.TextField(blank=True, verbose_name='NOC contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
field=models.URLField(blank=True, verbose_name='Portal'),
|
||||
),
|
||||
]
|
||||
20
netbox/circuits/migrations/0010_circuit_status.py
Normal file
20
netbox/circuits/migrations/0010_circuit_status.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-06 18:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0009_unicode_literals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from dcim.constants import STATUS_CLASSES
|
||||
from dcim.fields import ASNField
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from utilities.utils import csv_format
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
|
||||
TERM_SIDE_A = 'A'
|
||||
TERM_SIDE_Z = 'Z'
|
||||
TERM_SIDE_CHOICES = (
|
||||
(TERM_SIDE_A, 'A'),
|
||||
(TERM_SIDE_Z, 'Z'),
|
||||
)
|
||||
|
||||
|
||||
def humanize_speed(speed):
|
||||
"""
|
||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
||||
"""
|
||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||
return '{} Tbps'.format(speed / 1000000000)
|
||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
||||
return '{} Gbps'.format(speed / 1000000)
|
||||
elif speed >= 1000 and speed % 1000 == 0:
|
||||
return '{} Mbps'.format(speed / 1000)
|
||||
elif speed >= 1000:
|
||||
return '{} Mbps'.format(float(speed) / 1000)
|
||||
else:
|
||||
return '{} Kbps'.format(speed)
|
||||
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -50,6 +29,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -60,13 +41,16 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('circuits:provider', args=[self.slug])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.asn,
|
||||
self.account,
|
||||
self.portal_url,
|
||||
])
|
||||
self.noc_contact,
|
||||
self.admin_contact,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -78,6 +62,8 @@ class CircuitType(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -87,6 +73,12 @@ class CircuitType(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
@@ -98,6 +90,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
||||
status = models.PositiveSmallIntegerField(choices=CIRCUIT_STATUS_CHOICES, default=CIRCUIT_STATUS_ACTIVE)
|
||||
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
||||
@@ -105,26 +98,35 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} {}'.format(self.provider, self.cid)
|
||||
return '{} {}'.format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.cid,
|
||||
self.provider.name,
|
||||
self.type.name,
|
||||
self.get_status_display(),
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.install_date.isoformat() if self.install_date else None,
|
||||
self.install_date,
|
||||
self.commit_rate,
|
||||
self.description,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CLASSES[self.status]
|
||||
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
@@ -140,20 +142,20 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
def termination_z(self):
|
||||
return self._get_termination('Z')
|
||||
|
||||
def commit_rate_human(self):
|
||||
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
|
||||
commit_rate_human.admin_order_field = 'commit_rate'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CircuitTermination(models.Model):
|
||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
||||
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
||||
interface = models.OneToOneField('dcim.Interface', related_name='circuit_termination', blank=True, null=True)
|
||||
interface = models.OneToOneField(
|
||||
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
upstream_speed = models.PositiveIntegerField(
|
||||
blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed'
|
||||
)
|
||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||
|
||||
@@ -162,7 +164,7 @@ class CircuitTermination(models.Model):
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
@@ -170,11 +172,3 @@ class CircuitTermination(models.Model):
|
||||
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
|
||||
except CircuitTermination.DoesNotExist:
|
||||
return None
|
||||
|
||||
def port_speed_human(self):
|
||||
return humanize_speed(self.port_speed)
|
||||
port_speed_human.admin_order_field = 'port_speed'
|
||||
|
||||
def upstream_speed_human(self):
|
||||
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
|
||||
upstream_speed_human.admin_order_field = 'upstream_speed'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
|
||||
|
||||
class CircuitTerminationColumn(tables.Column):
|
||||
|
||||
def render(self, value):
|
||||
if value.interface:
|
||||
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
|
||||
value.interface.device.get_absolute_url(),
|
||||
value.site,
|
||||
value.interface.device
|
||||
))
|
||||
return mark_safe('<a href="{}">{}</a>'.format(
|
||||
value.site.get_absolute_url(),
|
||||
value.site
|
||||
))
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
@@ -19,12 +40,17 @@ CIRCUITTYPE_ACTIONS = """
|
||||
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
account = tables.Column(verbose_name='Account')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
name = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = ('pk', 'name', 'asn', 'account',)
|
||||
|
||||
|
||||
class ProviderDetailTable(ProviderTable):
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta(ProviderTable.Meta):
|
||||
model = Provider
|
||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
@@ -35,11 +61,11 @@ class ProviderTable(BaseTable):
|
||||
|
||||
class CircuitTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
@@ -52,16 +78,13 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
||||
type = tables.Column(verbose_name='Type')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||
args=[Accessor('termination_a.site.slug')])
|
||||
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||
args=[Accessor('termination_z.site.slug')])
|
||||
description = tables.Column(verbose_name='Description')
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
||||
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')
|
||||
|
||||
0
netbox/circuits/tests/__init__.py
Normal file
0
netbox/circuits/tests/__init__.py
Normal file
387
netbox/circuits/tests/test_api.py
Normal file
387
netbox/circuits/tests/test_api.py
Normal file
@@ -0,0 +1,387 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Site
|
||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||
from extras.models import Graph
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
class ProviderTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
self.provider3 = Provider.objects.create(name='Test Provider 3', slug='test-provider-3')
|
||||
|
||||
def test_get_provider(self):
|
||||
|
||||
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.provider1.name)
|
||||
|
||||
def test_get_provider_graphs(self):
|
||||
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 1',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
|
||||
)
|
||||
self.graph2 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 2',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
|
||||
)
|
||||
self.graph3 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 3',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
|
||||
)
|
||||
|
||||
url = reverse('circuits-api:provider-graphs', kwargs={'pk': self.provider1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=test-provider-1&foo=1')
|
||||
|
||||
def test_list_providers(self):
|
||||
|
||||
url = reverse('circuits-api:provider-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_create_provider(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Provider 4',
|
||||
'slug': 'test-provider-4',
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:provider-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Provider.objects.count(), 4)
|
||||
provider4 = Provider.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(provider4.name, data['name'])
|
||||
self.assertEqual(provider4.slug, data['slug'])
|
||||
|
||||
def test_create_provider_bulk(self):
|
||||
|
||||
data = [
|
||||
{
|
||||
'name': 'Test Provider 4',
|
||||
'slug': 'test-provider-4',
|
||||
},
|
||||
{
|
||||
'name': 'Test Provider 5',
|
||||
'slug': 'test-provider-5',
|
||||
},
|
||||
{
|
||||
'name': 'Test Provider 6',
|
||||
'slug': 'test-provider-6',
|
||||
},
|
||||
]
|
||||
|
||||
url = reverse('circuits-api:provider-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Provider.objects.count(), 6)
|
||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||
|
||||
def test_update_provider(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Provider X',
|
||||
'slug': 'test-provider-x',
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(Provider.objects.count(), 3)
|
||||
provider1 = Provider.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(provider1.name, data['name'])
|
||||
self.assertEqual(provider1.slug, data['slug'])
|
||||
|
||||
def test_delete_provider(self):
|
||||
|
||||
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(Provider.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
|
||||
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
|
||||
self.circuittype3 = CircuitType.objects.create(name='Test Circuit Type 3', slug='test-circuit-type-3')
|
||||
|
||||
def test_get_circuittype(self):
|
||||
|
||||
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.circuittype1.name)
|
||||
|
||||
def test_list_circuittypes(self):
|
||||
|
||||
url = reverse('circuits-api:circuittype-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_create_circuittype(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Circuit Type 4',
|
||||
'slug': 'test-circuit-type-4',
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuittype-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(CircuitType.objects.count(), 4)
|
||||
circuittype4 = CircuitType.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittype4.name, data['name'])
|
||||
self.assertEqual(circuittype4.slug, data['slug'])
|
||||
|
||||
def test_update_circuittype(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Circuit Type X',
|
||||
'slug': 'test-circuit-type-x',
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(CircuitType.objects.count(), 3)
|
||||
circuittype1 = CircuitType.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittype1.name, data['name'])
|
||||
self.assertEqual(circuittype1.slug, data['slug'])
|
||||
|
||||
def test_delete_circuittype(self):
|
||||
|
||||
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(CircuitType.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
|
||||
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
|
||||
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=self.provider1, type=self.circuittype1)
|
||||
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=self.provider1, type=self.circuittype1)
|
||||
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=self.provider1, type=self.circuittype1)
|
||||
|
||||
def test_get_circuit(self):
|
||||
|
||||
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['cid'], self.circuit1.cid)
|
||||
|
||||
def test_list_circuits(self):
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_create_circuit(self):
|
||||
|
||||
data = {
|
||||
'cid': 'TEST0004',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Circuit.objects.count(), 4)
|
||||
circuit4 = Circuit.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuit4.cid, data['cid'])
|
||||
self.assertEqual(circuit4.provider_id, data['provider'])
|
||||
self.assertEqual(circuit4.type_id, data['type'])
|
||||
|
||||
def test_create_circuit_bulk(self):
|
||||
|
||||
data = [
|
||||
{
|
||||
'cid': 'TEST0004',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
},
|
||||
{
|
||||
'cid': 'TEST0005',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
},
|
||||
{
|
||||
'cid': 'TEST0006',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
},
|
||||
]
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Circuit.objects.count(), 6)
|
||||
self.assertEqual(response.data[0]['cid'], data[0]['cid'])
|
||||
self.assertEqual(response.data[1]['cid'], data[1]['cid'])
|
||||
self.assertEqual(response.data[2]['cid'], data[2]['cid'])
|
||||
|
||||
def test_update_circuit(self):
|
||||
|
||||
data = {
|
||||
'cid': 'TEST000X',
|
||||
'provider': self.provider2.pk,
|
||||
'type': self.circuittype2.pk,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(Circuit.objects.count(), 3)
|
||||
circuit1 = Circuit.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuit1.cid, data['cid'])
|
||||
self.assertEqual(circuit1.provider_id, data['provider'])
|
||||
self.assertEqual(circuit1.type_id, data['type'])
|
||||
|
||||
def test_delete_circuit(self):
|
||||
|
||||
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(Circuit.objects.count(), 2)
|
||||
|
||||
|
||||
class CircuitTerminationTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
|
||||
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
|
||||
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
|
||||
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
|
||||
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination2 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination3 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
|
||||
def test_get_circuittermination(self):
|
||||
|
||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['id'], self.circuittermination1.pk)
|
||||
|
||||
def test_list_circuitterminations(self):
|
||||
|
||||
url = reverse('circuits-api:circuittermination-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_create_circuittermination(self):
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit1.pk,
|
||||
'term_side': TERM_SIDE_Z,
|
||||
'site': self.site2.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuittermination-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 4)
|
||||
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
|
||||
self.assertEqual(circuittermination4.term_side, data['term_side'])
|
||||
self.assertEqual(circuittermination4.site_id, data['site'])
|
||||
self.assertEqual(circuittermination4.port_speed, data['port_speed'])
|
||||
|
||||
def test_update_circuittermination(self):
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit1.pk,
|
||||
'term_side': TERM_SIDE_Z,
|
||||
'site': self.site2.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 3)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittermination1.circuit_id, data['circuit'])
|
||||
self.assertEqual(circuittermination1.term_side, data['term_side'])
|
||||
self.assertEqual(circuittermination1.site_id, data['site'])
|
||||
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
|
||||
|
||||
def test_delete_circuittermination(self):
|
||||
|
||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 2)
|
||||
@@ -1,39 +1,42 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
|
||||
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
|
||||
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||
from .constants import TERM_SIDE_A, TERM_SIDE_Z
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
#
|
||||
@@ -24,31 +27,41 @@ class ProviderListView(ObjectListView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filter = filters.ProviderFilter
|
||||
filter_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderTable
|
||||
table = tables.ProviderDetailTable
|
||||
template_name = 'circuits/provider_list.html'
|
||||
|
||||
|
||||
def provider(request, slug):
|
||||
class ProviderView(View):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
def get(self, request, slug):
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related(
|
||||
'type', 'tenant'
|
||||
).prefetch_related(
|
||||
'terminations__site'
|
||||
)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
||||
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
model = Provider
|
||||
form_class = forms.ProviderForm
|
||||
model_form = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderEditView(ProviderCreateView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
|
||||
|
||||
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
model = Provider
|
||||
@@ -57,9 +70,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
form = forms.ProviderImportForm
|
||||
model_form = forms.ProviderCSVForm
|
||||
table = tables.ProviderTable
|
||||
template_name = 'circuits/provider_import.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -67,8 +79,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
cls = Provider
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
template_name = 'circuits/provider_bulk_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -76,6 +88,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -89,18 +102,31 @@ class CircuitTypeListView(ObjectListView):
|
||||
template_name = 'circuits/circuittype_list.html'
|
||||
|
||||
|
||||
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittype'
|
||||
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_circuittype'
|
||||
model = CircuitType
|
||||
form_class = forms.CircuitTypeForm
|
||||
model_form = forms.CircuitTypeForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('circuits:circuittype_list')
|
||||
|
||||
|
||||
class CircuitTypeEditView(CircuitTypeCreateView):
|
||||
permission_required = 'circuits.change_circuittype'
|
||||
|
||||
|
||||
class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuittype'
|
||||
model_form = forms.CircuitTypeCSVForm
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = CircuitType
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
@@ -109,35 +135,52 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class CircuitListView(ObjectListView):
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
queryset = Circuit.objects.select_related(
|
||||
'provider', 'type', 'tenant'
|
||||
).prefetch_related(
|
||||
'terminations__site', 'terminations__interface__device'
|
||||
)
|
||||
filter = filters.CircuitFilter
|
||||
filter_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
template_name = 'circuits/circuit_list.html'
|
||||
|
||||
|
||||
def circuit(request, pk):
|
||||
class CircuitView(View):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
||||
def get(self, request, pk):
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
})
|
||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||
termination_a = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
})
|
||||
|
||||
|
||||
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
model = Circuit
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['provider']
|
||||
model_form = forms.CircuitForm
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitEditView(CircuitCreateView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
|
||||
|
||||
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
model = Circuit
|
||||
@@ -146,25 +189,27 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
form = forms.CircuitImportForm
|
||||
model_form = forms.CircuitCSVForm
|
||||
table = tables.CircuitTable
|
||||
template_name = 'circuits/circuit_import.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
template_name = 'circuits/circuit_bulk_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
@@ -217,22 +262,25 @@ def circuit_terminations_swap(request, pk):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_circuittermination'
|
||||
model = CircuitTermination
|
||||
form_class = forms.CircuitTerminationForm
|
||||
fields_initial = ['term_side']
|
||||
model_form = forms.CircuitTerminationForm
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'circuit' in kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'circuit' in url_kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.circuit.get_absolute_url()
|
||||
|
||||
|
||||
class CircuitTerminationEditView(CircuitTerminationCreateView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
|
||||
|
||||
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_circuittermination'
|
||||
model = CircuitTermination
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'facility', 'asn']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackGroup)
|
||||
class RackGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackRole)
|
||||
class RackRoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'color']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Rack)
|
||||
class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
class ConsolePortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class PowerPortTemplateAdmin(admin.TabularInline):
|
||||
model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerOutletTemplateAdmin(admin.TabularInline):
|
||||
model = PowerOutletTemplate
|
||||
|
||||
|
||||
class InterfaceTemplateAdmin(admin.TabularInline):
|
||||
model = InterfaceTemplate
|
||||
|
||||
|
||||
class DeviceBayTemplateAdmin(admin.TabularInline):
|
||||
model = DeviceBayTemplate
|
||||
|
||||
|
||||
@admin.register(DeviceType)
|
||||
class DeviceTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['model'],
|
||||
}
|
||||
inlines = [
|
||||
ConsolePortTemplateAdmin,
|
||||
ConsoleServerPortTemplateAdmin,
|
||||
PowerPortTemplateAdmin,
|
||||
PowerOutletTemplateAdmin,
|
||||
InterfaceTemplateAdmin,
|
||||
DeviceBayTemplateAdmin,
|
||||
]
|
||||
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
|
||||
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
|
||||
list_filter = ['manufacturer']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return DeviceType.objects.annotate(
|
||||
console_port_count=Count('console_port_templates', distinct=True),
|
||||
cs_port_count=Count('cs_port_templates', distinct=True),
|
||||
power_port_count=Count('power_port_templates', distinct=True),
|
||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||
interface_count=Count('interface_templates', distinct=True),
|
||||
devicebay_count=Count('device_bay_templates', distinct=True),
|
||||
)
|
||||
|
||||
def console_ports(self, instance):
|
||||
return instance.console_port_count
|
||||
|
||||
def console_server_ports(self, instance):
|
||||
return instance.cs_port_count
|
||||
|
||||
def power_ports(self, instance):
|
||||
return instance.power_port_count
|
||||
|
||||
def power_outlets(self, instance):
|
||||
return instance.power_outlet_count
|
||||
|
||||
def interfaces(self, instance):
|
||||
return instance.interface_count
|
||||
|
||||
def device_bays(self, instance):
|
||||
return instance.devicebay_count
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@admin.register(DeviceRole)
|
||||
class DeviceRoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
@admin.register(Platform)
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'rpc_client']
|
||||
|
||||
|
||||
class ConsolePortAdmin(admin.TabularInline):
|
||||
model = ConsolePort
|
||||
readonly_fields = ['cs_port']
|
||||
|
||||
|
||||
class ConsoleServerPortAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class PowerPortAdmin(admin.TabularInline):
|
||||
model = PowerPort
|
||||
readonly_fields = ['power_outlet']
|
||||
|
||||
|
||||
class PowerOutletAdmin(admin.TabularInline):
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class InterfaceAdmin(admin.TabularInline):
|
||||
model = Interface
|
||||
|
||||
|
||||
class DeviceBayAdmin(admin.TabularInline):
|
||||
model = DeviceBay
|
||||
fk_name = 'device'
|
||||
readonly_fields = ['installed_device']
|
||||
|
||||
|
||||
class ModuleAdmin(admin.TabularInline):
|
||||
model = Module
|
||||
readonly_fields = ['parent', 'discovered']
|
||||
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ConsolePortAdmin,
|
||||
ConsoleServerPortAdmin,
|
||||
PowerPortAdmin,
|
||||
PowerOutletAdmin,
|
||||
InterfaceAdmin,
|
||||
DeviceBayAdmin,
|
||||
ModuleAdmin,
|
||||
]
|
||||
list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
|
||||
'serial']
|
||||
list_filter = ['device_role']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||
|
||||
def device_type_full_name(self, obj):
|
||||
return obj.device_type.full_name
|
||||
device_type_full_name.short_description = 'Device type'
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,76 +1,70 @@
|
||||
from django.conf.urls import url
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.api.views import GraphListView, TopologyMapView
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import *
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
class DCIMRootView(routers.APIRootView):
|
||||
"""
|
||||
DCIM API root view
|
||||
"""
|
||||
def get_view_name(self):
|
||||
return 'DCIM'
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
||||
url(r'^sites/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'),
|
||||
url(r'^sites/(?P<site>\d+)/racks/$', RackListView.as_view(), name='site_racks'),
|
||||
|
||||
# Rack groups
|
||||
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = DCIMRootView
|
||||
|
||||
# Rack roles
|
||||
url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
|
||||
# Field choices
|
||||
router.register(r'_choices', views.DCIMFieldChoicesViewSet, base_name='field-choice')
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
||||
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
|
||||
# Sites
|
||||
router.register(r'regions', views.RegionViewSet)
|
||||
router.register(r'sites', views.SiteViewSet)
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
||||
# Racks
|
||||
router.register(r'rack-groups', views.RackGroupViewSet)
|
||||
router.register(r'rack-roles', views.RackRoleViewSet)
|
||||
router.register(r'racks', views.RackViewSet)
|
||||
router.register(r'rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
# Device types
|
||||
url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'),
|
||||
# Device types
|
||||
router.register(r'manufacturers', views.ManufacturerViewSet)
|
||||
router.register(r'device-types', views.DeviceTypeViewSet)
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'),
|
||||
# Device type components
|
||||
router.register(r'console-port-templates', views.ConsolePortTemplateViewSet)
|
||||
router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
|
||||
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
|
||||
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
|
||||
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
|
||||
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||
|
||||
# Platforms
|
||||
url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'),
|
||||
url(r'^platforms/(?P<pk>\d+)/$', PlatformDetailView.as_view(), name='platform_detail'),
|
||||
# Devices
|
||||
router.register(r'device-roles', views.DeviceRoleViewSet)
|
||||
router.register(r'platforms', views.PlatformViewSet)
|
||||
router.register(r'devices', views.DeviceViewSet)
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', DeviceListView.as_view(), name='device_list'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', DeviceDetailView.as_view(), name='device_detail'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(),
|
||||
name='device_consoleserverports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
|
||||
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
|
||||
url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
|
||||
# Device components
|
||||
router.register(r'console-ports', views.ConsolePortViewSet)
|
||||
router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
|
||||
router.register(r'power-ports', views.PowerPortViewSet)
|
||||
router.register(r'power-outlets', views.PowerOutletViewSet)
|
||||
router.register(r'interfaces', views.InterfaceViewSet)
|
||||
router.register(r'device-bays', views.DeviceBayViewSet)
|
||||
router.register(r'inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Console ports
|
||||
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
||||
# Connections
|
||||
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
|
||||
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||
|
||||
# Power ports
|
||||
url(r'^power-ports/(?P<pk>\d+)/$', PowerPortView.as_view(), name='powerport'),
|
||||
# Virtual chassis
|
||||
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||
|
||||
# Interfaces
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
|
||||
name='interface_graphs'),
|
||||
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||
url(r'^topology-maps/(?P<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
|
||||
|
||||
]
|
||||
app_name = 'dcim-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,121 +1,115 @@
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
)
|
||||
from dcim import filters
|
||||
from extras.api.views import CustomFieldModelAPIView
|
||||
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
||||
from utilities.api import ServiceUnavailable
|
||||
from .exceptions import MissingFilterException
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
)
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Device, ['face', 'status']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Interface, ['form_factor']),
|
||||
(InterfaceConnection, ['connection_status']),
|
||||
(InterfaceTemplate, ['form_factor']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['type', 'width']),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionViewSet(ModelViewSet):
|
||||
queryset = Region.objects.all()
|
||||
serializer_class = serializers.RegionSerializer
|
||||
write_serializer_class = serializers.WritableRegionSerializer
|
||||
filter_class = filters.RegionFilter
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List all sites
|
||||
"""
|
||||
queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
|
||||
class SiteViewSet(CustomFieldModelViewSet):
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
serializer_class = serializers.SiteSerializer
|
||||
write_serializer_class = serializers.WritableSiteSerializer
|
||||
filter_class = filters.SiteFilter
|
||||
|
||||
|
||||
class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single site
|
||||
"""
|
||||
queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.SiteSerializer
|
||||
@detail_route()
|
||||
def graphs(self, request, pk=None):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular site.
|
||||
"""
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack groups
|
||||
"""
|
||||
class RackGroupViewSet(ModelViewSet):
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
write_serializer_class = serializers.WritableRackGroupSerializer
|
||||
filter_class = filters.RackGroupFilter
|
||||
|
||||
|
||||
class RackGroupDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack group
|
||||
"""
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack roles
|
||||
"""
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
|
||||
|
||||
class RackRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack role
|
||||
"""
|
||||
class RackRoleViewSet(ModelViewSet):
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
filter_class = filters.RackRoleFilter
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List racks (filterable)
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
class RackViewSet(CustomFieldModelViewSet):
|
||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
||||
serializer_class = serializers.RackSerializer
|
||||
write_serializer_class = serializers.WritableRackSerializer
|
||||
filter_class = filters.RackFilter
|
||||
|
||||
|
||||
class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.RackDetailSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack units
|
||||
#
|
||||
|
||||
class RackUnitListView(APIView):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
@detail_route()
|
||||
def units(self, request, pk=None):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 0)
|
||||
exclude_pk = request.GET.get('exclude', None)
|
||||
@@ -126,375 +120,320 @@ class RackUnitListView(APIView):
|
||||
exclude_pk = None
|
||||
elevation = rack.get_rack_units(face, exclude_pk)
|
||||
|
||||
# Serialize Devices within the rack elevation
|
||||
for u in elevation:
|
||||
if u['device']:
|
||||
u['device'] = serializers.DeviceNestedSerializer(instance=u['device']).data
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
|
||||
return Response(elevation)
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationViewSet(ModelViewSet):
|
||||
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
write_serializer_class = serializers.WritableRackReservationSerializer
|
||||
filter_class = filters.RackReservationFilter
|
||||
|
||||
# Assign user from request
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerListView(generics.ListAPIView):
|
||||
"""
|
||||
List all hardware manufacturers
|
||||
"""
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = serializers.ManufacturerSerializer
|
||||
|
||||
|
||||
class ManufacturerDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single hardware manufacturers
|
||||
"""
|
||||
class ManufacturerViewSet(ModelViewSet):
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = serializers.ManufacturerSerializer
|
||||
filter_class = filters.ManufacturerFilter
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List device types (filterable)
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
|
||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
write_serializer_class = serializers.WritableDeviceTypeSerializer
|
||||
filter_class = filters.DeviceTypeFilter
|
||||
|
||||
|
||||
class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device type
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.DeviceTypeDetailSerializer
|
||||
#
|
||||
# Device type components
|
||||
#
|
||||
|
||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
|
||||
filter_class = filters.ConsolePortTemplateFilter
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
|
||||
filter_class = filters.ConsoleServerPortTemplateFilter
|
||||
|
||||
|
||||
class PowerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerPortTemplateSerializer
|
||||
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
|
||||
filter_class = filters.PowerPortTemplateFilter
|
||||
|
||||
|
||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
|
||||
filter_class = filters.PowerOutletTemplateFilter
|
||||
|
||||
|
||||
class InterfaceTemplateViewSet(ModelViewSet):
|
||||
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.InterfaceTemplateSerializer
|
||||
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
|
||||
filter_class = filters.InterfaceTemplateFilter
|
||||
|
||||
|
||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
|
||||
filter_class = filters.DeviceBayTemplateFilter
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all device roles
|
||||
"""
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = serializers.DeviceRoleSerializer
|
||||
|
||||
|
||||
class DeviceRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device role
|
||||
"""
|
||||
class DeviceRoleViewSet(ModelViewSet):
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = serializers.DeviceRoleSerializer
|
||||
filter_class = filters.DeviceRoleFilter
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformListView(generics.ListAPIView):
|
||||
"""
|
||||
List all platforms
|
||||
"""
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
|
||||
|
||||
class PlatformDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single platform
|
||||
"""
|
||||
class PlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
write_serializer_class = serializers.WritablePlatformSerializer
|
||||
filter_class = filters.PlatformFilter
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List devices (filterable)
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||
'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
|
||||
'primary_ip6__nat_outside',
|
||||
'custom_field_values__field')
|
||||
class DeviceViewSet(CustomFieldModelViewSet):
|
||||
queryset = Device.objects.select_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master',
|
||||
).prefetch_related(
|
||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
||||
)
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
write_serializer_class = serializers.WritableDeviceSerializer
|
||||
filter_class = filters.DeviceFilter
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
||||
|
||||
|
||||
class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||
'rack__site', 'parent_bay').prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console ports (by device)
|
||||
"""
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsolePort.objects.filter(device=device).select_related('cs_port')
|
||||
|
||||
|
||||
class ConsolePortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
queryset = ConsolePort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console server ports (by device)
|
||||
"""
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List power ports (by device)
|
||||
"""
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerPort.objects.filter(device=device).select_related('power_outlet')
|
||||
|
||||
|
||||
class PowerPortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
queryset = PowerPort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletListView(generics.ListAPIView):
|
||||
"""
|
||||
List power outlets (by device)
|
||||
"""
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceListView(generics.ListAPIView):
|
||||
"""
|
||||
List interfaces (by device)
|
||||
"""
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filter_class = filters.InterfaceFilter
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
|
||||
|
||||
# Filter by type (physical or virtual)
|
||||
iface_type = self.request.query_params.get('type')
|
||||
if iface_type == 'physical':
|
||||
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||
elif iface_type == 'virtual':
|
||||
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
elif iface_type is not None:
|
||||
queryset = queryset.empty()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class InterfaceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single interface
|
||||
"""
|
||||
queryset = Interface.objects.select_related('device')
|
||||
serializer_class = serializers.InterfaceDetailSerializer
|
||||
|
||||
|
||||
class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
class InterfaceConnectionListView(generics.ListAPIView):
|
||||
"""
|
||||
Retrieve a list of all interface connections
|
||||
"""
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
||||
class DeviceBayListView(generics.ListAPIView):
|
||||
"""
|
||||
List device bays (by device)
|
||||
"""
|
||||
serializer_class = serializers.DeviceBayNestedSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return DeviceBay.objects.filter(device=device).select_related('installed_device')
|
||||
|
||||
|
||||
#
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleListView(generics.ListAPIView):
|
||||
"""
|
||||
List device modules (by device)
|
||||
"""
|
||||
serializer_class = serializers.ModuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return Module.objects.filter(device=device).select_related('device', 'manufacturer')
|
||||
|
||||
|
||||
#
|
||||
# Live queries
|
||||
#
|
||||
|
||||
class LLDPNeighborsView(APIView):
|
||||
"""
|
||||
Retrieve live LLDP neighbors of a device
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
@detail_route(url_path='napalm')
|
||||
def napalm(self, request, pk):
|
||||
"""
|
||||
Execute a NAPALM method on a Device
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable(detail="No IP configured for this device.")
|
||||
raise ServiceUnavailable("This device does not have a primary IP address configured.")
|
||||
if device.platform is None:
|
||||
raise ServiceUnavailable("No platform is configured for this device.")
|
||||
if not device.platform.napalm_driver:
|
||||
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
|
||||
device.platform
|
||||
))
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
raise ServiceUnavailable(detail="No RPC client available for this platform ({}).".format(device.platform))
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
# Check that NAPALM is installed
|
||||
try:
|
||||
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
|
||||
lldp_neighbors = rpc_client.get_lldp_neighbors()
|
||||
except:
|
||||
raise ServiceUnavailable(detail="Error connecting to the remote device.")
|
||||
import napalm
|
||||
except ImportError:
|
||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
|
||||
|
||||
return Response(lldp_neighbors)
|
||||
# Validate the configured driver
|
||||
try:
|
||||
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||
except ModuleImportError:
|
||||
raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
|
||||
device.platform, device.platform.napalm_driver
|
||||
))
|
||||
|
||||
# Verify user permission
|
||||
if not request.user.has_perm('dcim.napalm_read'):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
# Validate requested NAPALM methods
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
for method in napalm_methods:
|
||||
if not hasattr(driver, method):
|
||||
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
|
||||
elif not method.startswith('get_'):
|
||||
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
|
||||
|
||||
# Connect to the device and execute the requested methods
|
||||
# TODO: Improve error handling
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
ip_address = str(device.primary_ip.address.ip)
|
||||
d = driver(
|
||||
hostname=ip_address,
|
||||
username=settings.NAPALM_USERNAME,
|
||||
password=settings.NAPALM_PASSWORD,
|
||||
timeout=settings.NAPALM_TIMEOUT,
|
||||
optional_args=settings.NAPALM_ARGS
|
||||
)
|
||||
try:
|
||||
d.open()
|
||||
for method in napalm_methods:
|
||||
response[method] = getattr(d, method)()
|
||||
except Exception as e:
|
||||
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
|
||||
|
||||
d.close()
|
||||
return Response(response)
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortViewSet(ModelViewSet):
|
||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
write_serializer_class = serializers.WritableConsolePortSerializer
|
||||
filter_class = filters.ConsolePortFilter
|
||||
|
||||
|
||||
class ConsoleServerPortViewSet(ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
write_serializer_class = serializers.WritableConsoleServerPortSerializer
|
||||
filter_class = filters.ConsoleServerPortFilter
|
||||
|
||||
|
||||
class PowerPortViewSet(ModelViewSet):
|
||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
write_serializer_class = serializers.WritablePowerPortSerializer
|
||||
filter_class = filters.PowerPortFilter
|
||||
|
||||
|
||||
class PowerOutletViewSet(ModelViewSet):
|
||||
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
write_serializer_class = serializers.WritablePowerOutletSerializer
|
||||
filter_class = filters.PowerOutletFilter
|
||||
|
||||
|
||||
class InterfaceViewSet(ModelViewSet):
|
||||
queryset = Interface.objects.select_related('device')
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
write_serializer_class = serializers.WritableInterfaceSerializer
|
||||
filter_class = filters.InterfaceFilter
|
||||
|
||||
@detail_route()
|
||||
def graphs(self, request, pk=None):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular interface.
|
||||
"""
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class DeviceBayViewSet(ModelViewSet):
|
||||
queryset = DeviceBay.objects.select_related('installed_device')
|
||||
serializer_class = serializers.DeviceBaySerializer
|
||||
write_serializer_class = serializers.WritableDeviceBaySerializer
|
||||
filter_class = filters.DeviceBayFilter
|
||||
|
||||
|
||||
class InventoryItemViewSet(ModelViewSet):
|
||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||
serializer_class = serializers.InventoryItemSerializer
|
||||
write_serializer_class = serializers.WritableInventoryItemSerializer
|
||||
filter_class = filters.InventoryItemFilter
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filter_class = filters.ConsoleConnectionFilter
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filter_class = filters.PowerConnectionFilter
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ModelViewSet):
|
||||
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
|
||||
filter_class = filters.InterfaceConnectionFilter
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisViewSet(ModelViewSet):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
serializer_class = serializers.VirtualChassisSerializer
|
||||
write_serializer_class = serializers.WritableVirtualChassisSerializer
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
class RelatedConnectionsView(APIView):
|
||||
class ConnectedDeviceViewSet(ViewSet):
|
||||
"""
|
||||
Retrieve all connections related to a given console/power/interface connection
|
||||
This endpoint allows a user to determine what device (if any) is connected to a given peer device and peer
|
||||
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
|
||||
via a protocol such as LLDP. Two query parameters must be included in the request:
|
||||
|
||||
* `peer-device`: The name of the peer device
|
||||
* `peer-interface`: The name of the peer interface
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
|
||||
def __init__(self):
|
||||
super(RelatedConnectionsView, self).__init__()
|
||||
def get_view_name(self):
|
||||
return "Connected Device Locator"
|
||||
|
||||
# Custom fields
|
||||
self.content_type = ContentType.objects.get_for_model(Device)
|
||||
self.custom_fields = self.content_type.custom_fields.prefetch_related('choices')
|
||||
def list(self, request):
|
||||
|
||||
def get(self, request):
|
||||
peer_device_name = request.query_params.get('peer-device')
|
||||
peer_interface_name = request.query_params.get('peer-interface')
|
||||
if not peer_device_name or not peer_interface_name:
|
||||
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
||||
|
||||
peer_device = request.GET.get('peer-device')
|
||||
peer_interface = request.GET.get('peer-interface')
|
||||
# Determine local interface from peer interface's connection
|
||||
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
||||
local_interface = peer_interface.connected_interface
|
||||
|
||||
# Search by interface
|
||||
if peer_device and peer_interface:
|
||||
if local_interface is None:
|
||||
return Response()
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
try:
|
||||
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
||||
except Interface.DoesNotExist:
|
||||
raise Http404()
|
||||
local_iface = peer_iface.connected_interface
|
||||
if local_iface:
|
||||
device = local_iface.device
|
||||
else:
|
||||
return Response()
|
||||
|
||||
else:
|
||||
raise MissingFilterException(detail='Must specify search parameters "peer-device" and "peer-interface".')
|
||||
|
||||
# Initialize response skeleton
|
||||
response = {
|
||||
'device': serializers.DeviceSerializer(device, context={'view': self}).data,
|
||||
'console-ports': [],
|
||||
'power-ports': [],
|
||||
'interfaces': [],
|
||||
}
|
||||
|
||||
# Console connections
|
||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||
for cp in console_ports:
|
||||
data = serializers.ConsolePortSerializer(instance=cp).data
|
||||
del(data['device'])
|
||||
response['console-ports'].append(data)
|
||||
|
||||
# Power connections
|
||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||
for pp in power_ports:
|
||||
data = serializers.PowerPortSerializer(instance=pp).data
|
||||
del(data['device'])
|
||||
response['power-ports'].append(data)
|
||||
|
||||
# Interface connections
|
||||
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
|
||||
for iface in interfaces:
|
||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||
del(data['device'])
|
||||
response['interfaces'].append(data)
|
||||
|
||||
return Response(response)
|
||||
return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DCIMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
||||
|
||||
def ready(self):
|
||||
import dcim.signals
|
||||
|
||||
257
netbox/dcim/constants.py
Normal file
257
netbox/dcim/constants.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
# Rack types
|
||||
RACK_TYPE_2POST = 100
|
||||
RACK_TYPE_4POST = 200
|
||||
RACK_TYPE_CABINET = 300
|
||||
RACK_TYPE_WALLFRAME = 1000
|
||||
RACK_TYPE_WALLCABINET = 1100
|
||||
RACK_TYPE_CHOICES = (
|
||||
(RACK_TYPE_2POST, '2-post frame'),
|
||||
(RACK_TYPE_4POST, '4-post frame'),
|
||||
(RACK_TYPE_CABINET, '4-post cabinet'),
|
||||
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
|
||||
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
|
||||
)
|
||||
|
||||
# Rack widths
|
||||
RACK_WIDTH_19IN = 19
|
||||
RACK_WIDTH_23IN = 23
|
||||
RACK_WIDTH_CHOICES = (
|
||||
(RACK_WIDTH_19IN, '19 inches'),
|
||||
(RACK_WIDTH_23IN, '23 inches'),
|
||||
)
|
||||
|
||||
# Rack faces
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
[RACK_FACE_FRONT, 'Front'],
|
||||
[RACK_FACE_REAR, 'Rear'],
|
||||
]
|
||||
|
||||
# Parent/child device roles
|
||||
SUBDEVICE_ROLE_PARENT = True
|
||||
SUBDEVICE_ROLE_CHILD = False
|
||||
SUBDEVICE_ROLE_CHOICES = (
|
||||
(None, 'None'),
|
||||
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
)
|
||||
|
||||
# Interface ordering schemes (for device types)
|
||||
IFACE_ORDERING_POSITION = 1
|
||||
IFACE_ORDERING_NAME = 2
|
||||
IFACE_ORDERING_CHOICES = [
|
||||
[IFACE_ORDERING_POSITION, 'Slot/position'],
|
||||
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
|
||||
]
|
||||
|
||||
# Interface form factors
|
||||
# Virtual
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
IFACE_FF_LAG = 200
|
||||
# Ethernet
|
||||
IFACE_FF_100ME_FIXED = 800
|
||||
IFACE_FF_1GE_FIXED = 1000
|
||||
IFACE_FF_1GE_GBIC = 1050
|
||||
IFACE_FF_1GE_SFP = 1100
|
||||
IFACE_FF_10GE_FIXED = 1150
|
||||
IFACE_FF_10GE_CX4 = 1170
|
||||
IFACE_FF_10GE_SFP_PLUS = 1200
|
||||
IFACE_FF_10GE_XFP = 1300
|
||||
IFACE_FF_10GE_XENPAK = 1310
|
||||
IFACE_FF_10GE_X2 = 1320
|
||||
IFACE_FF_25GE_SFP28 = 1350
|
||||
IFACE_FF_40GE_QSFP_PLUS = 1400
|
||||
IFACE_FF_100GE_CFP = 1500
|
||||
IFACE_FF_100GE_CFP2 = 1510
|
||||
IFACE_FF_100GE_CFP4 = 1520
|
||||
IFACE_FF_100GE_CPAK = 1550
|
||||
IFACE_FF_100GE_QSFP28 = 1600
|
||||
# Wireless
|
||||
IFACE_FF_80211A = 2600
|
||||
IFACE_FF_80211G = 2610
|
||||
IFACE_FF_80211N = 2620
|
||||
IFACE_FF_80211AC = 2630
|
||||
IFACE_FF_80211AD = 2640
|
||||
# Fibrechannel
|
||||
IFACE_FF_1GFC_SFP = 3010
|
||||
IFACE_FF_2GFC_SFP = 3020
|
||||
IFACE_FF_4GFC_SFP = 3040
|
||||
IFACE_FF_8GFC_SFP_PLUS = 3080
|
||||
IFACE_FF_16GFC_SFP_PLUS = 3160
|
||||
# Serial
|
||||
IFACE_FF_T1 = 4000
|
||||
IFACE_FF_E1 = 4010
|
||||
IFACE_FF_T3 = 4040
|
||||
IFACE_FF_E3 = 4050
|
||||
# Stacking
|
||||
IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
IFACE_FF_JUNIPER_VCP = 5200
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
IFACE_FF_CHOICES = [
|
||||
[
|
||||
'Virtual interfaces',
|
||||
[
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Ethernet (fixed)',
|
||||
[
|
||||
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
|
||||
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
|
||||
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
|
||||
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Ethernet (modular)',
|
||||
[
|
||||
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
|
||||
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
|
||||
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
|
||||
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
|
||||
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
|
||||
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
|
||||
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
||||
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
|
||||
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
|
||||
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Wireless',
|
||||
[
|
||||
[IFACE_FF_80211A, 'IEEE 802.11a'],
|
||||
[IFACE_FF_80211G, 'IEEE 802.11b/g'],
|
||||
[IFACE_FF_80211N, 'IEEE 802.11n'],
|
||||
[IFACE_FF_80211AC, 'IEEE 802.11ac'],
|
||||
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'FibreChannel',
|
||||
[
|
||||
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
|
||||
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
|
||||
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
|
||||
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Serial',
|
||||
[
|
||||
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
|
||||
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Stacking',
|
||||
[
|
||||
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Other',
|
||||
[
|
||||
[IFACE_FF_OTHER, 'Other'],
|
||||
]
|
||||
],
|
||||
]
|
||||
|
||||
VIRTUAL_IFACE_TYPES = [
|
||||
IFACE_FF_VIRTUAL,
|
||||
IFACE_FF_LAG,
|
||||
]
|
||||
|
||||
WIRELESS_IFACE_TYPES = [
|
||||
IFACE_FF_80211A,
|
||||
IFACE_FF_80211G,
|
||||
IFACE_FF_80211N,
|
||||
IFACE_FF_80211AC,
|
||||
IFACE_FF_80211AD,
|
||||
]
|
||||
|
||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
IFACE_MODE_ACCESS = 100
|
||||
IFACE_MODE_TAGGED = 200
|
||||
IFACE_MODE_TAGGED_ALL = 300
|
||||
IFACE_MODE_CHOICES = [
|
||||
[IFACE_MODE_ACCESS, 'Access'],
|
||||
[IFACE_MODE_TAGGED, 'Tagged'],
|
||||
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
|
||||
]
|
||||
|
||||
# Device statuses
|
||||
DEVICE_STATUS_OFFLINE = 0
|
||||
DEVICE_STATUS_ACTIVE = 1
|
||||
DEVICE_STATUS_PLANNED = 2
|
||||
DEVICE_STATUS_STAGED = 3
|
||||
DEVICE_STATUS_FAILED = 4
|
||||
DEVICE_STATUS_INVENTORY = 5
|
||||
DEVICE_STATUS_CHOICES = [
|
||||
[DEVICE_STATUS_ACTIVE, 'Active'],
|
||||
[DEVICE_STATUS_OFFLINE, 'Offline'],
|
||||
[DEVICE_STATUS_PLANNED, 'Planned'],
|
||||
[DEVICE_STATUS_STAGED, 'Staged'],
|
||||
[DEVICE_STATUS_FAILED, 'Failed'],
|
||||
[DEVICE_STATUS_INVENTORY, 'Inventory'],
|
||||
]
|
||||
|
||||
# Site statuses
|
||||
SITE_STATUS_ACTIVE = 1
|
||||
SITE_STATUS_PLANNED = 2
|
||||
SITE_STATUS_RETIRED = 4
|
||||
SITE_STATUS_CHOICES = [
|
||||
[SITE_STATUS_ACTIVE, 'Active'],
|
||||
[SITE_STATUS_PLANNED, 'Planned'],
|
||||
[SITE_STATUS_RETIRED, 'Retired'],
|
||||
]
|
||||
|
||||
# Bootstrap CSS classes for device statuses
|
||||
STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
4: 'danger',
|
||||
5: 'default',
|
||||
}
|
||||
|
||||
# Console/power/interface connection statuses
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
[CONNECTION_STATUS_PLANNED, 'Planned'],
|
||||
[CONNECTION_STATUS_CONNECTED, 'Connected'],
|
||||
]
|
||||
|
||||
# Platform -> RPC client mappings
|
||||
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
|
||||
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
|
||||
RPC_CLIENT_OPENGEAR = 'opengear'
|
||||
RPC_CLIENT_CHOICES = [
|
||||
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
|
||||
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
|
||||
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
|
||||
]
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import EUI, mac_unix_expanded
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -1,29 +1,83 @@
|
||||
import django_filters
|
||||
from netaddr.core import AddrFormatError
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
||||
from virtualization.models import Cluster
|
||||
from .constants import (
|
||||
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
|
||||
WIRELESS_IFACE_TYPES,
|
||||
)
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
class RegionFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
name='parent__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Parent region (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='region__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -31,11 +85,22 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['q', 'name', 'facility', 'asn']
|
||||
fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
||||
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(facility__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(physical_address__icontains=value) |
|
||||
Q(shipping_address__icontains=value) |
|
||||
Q(contact_name__icontains=value) |
|
||||
Q(contact_phone__icontains=value) |
|
||||
Q(contact_email__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
except ValueError:
|
||||
@@ -45,7 +110,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
@@ -58,15 +122,24 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'name', 'slug']
|
||||
|
||||
|
||||
class RackRoleFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
facility_id = NullableCharFieldFilter()
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
@@ -76,35 +149,32 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role__slug',
|
||||
queryset=RackRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
@@ -112,21 +182,187 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['u_height']
|
||||
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(facility_id__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
name='user',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['created']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(rack__name__icontains=value) |
|
||||
Q(rack__facility_id__icontains=value) |
|
||||
Q(user__username__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
name='device_type_id',
|
||||
label='Device type (ID)',
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class DeviceRoleFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilter(django_filters.FilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -140,35 +376,70 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
|
||||
'subdevice_role']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
model = Platform
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
mac_address = django_filters.MethodFilter(
|
||||
action='_mac_address',
|
||||
label='MAC address',
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role_id',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
name = NullableCharFieldFilter()
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
@@ -183,43 +454,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type (ID)',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cluster.objects.all(),
|
||||
label='VM cluster (ID)',
|
||||
)
|
||||
model = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__slug',
|
||||
@@ -227,20 +464,13 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
)
|
||||
platform_id = NullableModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=DEVICE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
platform = NullableModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
status = django_filters.BooleanFilter(
|
||||
name='status',
|
||||
label='Status',
|
||||
is_full_depth = django_filters.BooleanFilter(
|
||||
name='device_type__is_full_depth',
|
||||
label='Is full depth',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
@@ -254,96 +484,92 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
name='device_type__is_network_device',
|
||||
label='Is a network device',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label='Has a primary IP',
|
||||
)
|
||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='virtual_chassis',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual chassis (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'serial', 'asset_tag']
|
||||
fields = ['serial', 'position']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(modules__serial__icontains=value.strip()) |
|
||||
Q(inventory_items__serial__icontains=value.strip()) |
|
||||
Q(asset_tag=value.strip()) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def _mac_address(self, queryset, value):
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(interfaces__mac_address=value).distinct()
|
||||
mac = EUI(value.strip())
|
||||
return queryset.filter(interfaces__mac_address=mac).distinct()
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(
|
||||
Q(primary_ip4__isnull=False) |
|
||||
Q(primary_ip6__isnull=False)
|
||||
)
|
||||
else:
|
||||
return queryset.exclude(
|
||||
Q(primary_ip4__isnull=False) |
|
||||
Q(primary_ip6__isnull=False)
|
||||
)
|
||||
|
||||
class ConsolePortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
device = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
class PowerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerOutletFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@@ -351,69 +577,201 @@ class PowerOutletFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
"""
|
||||
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
|
||||
Device's DeviceType.
|
||||
"""
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
name='name',
|
||||
label='Device',
|
||||
)
|
||||
device_id = django_filters.NumberFilter(
|
||||
method='filter_device',
|
||||
name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
type = django_filters.CharFilter(
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
label='LAG interface (ID)',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
device = Device.objects.select_related('device_type').get(**{name: value})
|
||||
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
|
||||
ordering = device.device_type.interface_ordering
|
||||
return queryset.filter(pk__in=vc_interface_ids).order_naturally(ordering)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
value = value.strip().lower()
|
||||
return {
|
||||
'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
|
||||
'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
|
||||
'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
|
||||
'lag': queryset.filter(form_factor=IFACE_FF_LAG),
|
||||
}.get(value, queryset.none())
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
mac = EUI(value.strip())
|
||||
return queryset.filter(mac_address=mac)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(part_id__icontains=value) |
|
||||
Q(serial__iexact=value) |
|
||||
Q(asset_tag__iexact=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class VirtualChassisFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['domain']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
model = ConsolePort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__rack__site__slug=value)
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cs_port__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
model = PowerPort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__rack__site__slug=value)
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(power_outlet__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['connection_status']
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__rack__site__slug=value) |
|
||||
Q(interface_b__device__rack__site__slug=value)
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__name__icontains=value) |
|
||||
Q(interface_b__device__name__icontains=value)
|
||||
)
|
||||
|
||||
@@ -1915,6 +1915,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-edge1",
|
||||
"serial": "5555555555",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 1,
|
||||
"face": 0,
|
||||
@@ -1935,6 +1936,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-core1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 17,
|
||||
"face": 0,
|
||||
@@ -1955,6 +1957,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-spine1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 33,
|
||||
"face": 0,
|
||||
@@ -1975,6 +1978,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-leaf1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 34,
|
||||
"face": 0,
|
||||
@@ -1995,6 +1999,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-leaf2",
|
||||
"serial": "9823478293748",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 34,
|
||||
"face": 0,
|
||||
@@ -2015,6 +2020,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-spine2",
|
||||
"serial": "45649818158",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 33,
|
||||
"face": 0,
|
||||
@@ -2035,6 +2041,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-edge2",
|
||||
"serial": "7567356345",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 1,
|
||||
"face": 0,
|
||||
@@ -2055,6 +2062,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-core2",
|
||||
"serial": "67856734534",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 17,
|
||||
"face": 0,
|
||||
@@ -2075,6 +2083,7 @@
|
||||
"platform": 2,
|
||||
"name": "test1-oob1",
|
||||
"serial": "98273942938",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 42,
|
||||
"face": 0,
|
||||
@@ -2095,6 +2104,7 @@
|
||||
"platform": null,
|
||||
"name": "test1-pdu1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": null,
|
||||
"face": null,
|
||||
@@ -2115,6 +2125,7 @@
|
||||
"platform": null,
|
||||
"name": "test1-pdu2",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": null,
|
||||
"face": null,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from netaddr import EUI, AddrFormatError
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from netaddr import EUI, AddrFormatError
|
||||
|
||||
|
||||
#
|
||||
|
||||
2306
netbox/dcim/forms.py
2306
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 18:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('dcim', '0025_devicetype_add_interface_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RackReservation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('description', models.CharField(max_length=100)),
|
||||
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0026_add_rack_reservations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
]
|
||||
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copy_site_from_rack(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for device in Device.objects.all():
|
||||
device.site = device.rack.site
|
||||
device.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0027_device_add_site'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_site_from_rack),
|
||||
]
|
||||
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0028_device_copy_rack_to_site'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
]
|
||||
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-27 19:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0029_allow_rackless_devices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name=b'Parent LAG'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
38
netbox/dcim/migrations/0031_regions.py
Normal file
38
netbox/dcim/migrations/0031_regions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-28 17:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0030_interface_add_lag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-03-02 15:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0031_regions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0033_rackreservation_rack_editable.py
Normal file
21
netbox/dcim/migrations/0033_rackreservation_rack_editable.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-17 18:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0032_device_increase_name_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-21 14:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0033_rackreservation_rack_editable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Module',
|
||||
new_name='InventoryItem',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
|
||||
),
|
||||
]
|
||||
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-08 15:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0034_rename_module_to_inventoryitem'),
|
||||
]
|
||||
|
||||
# We convert the BooleanField to an IntegerField first as PostgreSQL does not provide a direct cast for boolean to
|
||||
# smallint (attempting to convert directly yields the error "cannot cast type boolean to smallint").
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-09 16:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0035_device_expand_status_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0036_add_ff_juniper_vcp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_console_server',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_full_depth',
|
||||
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_network_device',
|
||||
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_pdu',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfaceconnection',
|
||||
name='connection_status',
|
||||
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, verbose_name='Management only'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='discovered',
|
||||
field=models.BooleanField(default=False, verbose_name='Discovered'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='part_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0038_wireless_interfaces.py
Normal file
25
netbox/dcim/migrations/0038_wireless_interfaces.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-16 21:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0037_unicode_literals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0039_interface_add_enabled_mtu.py
Normal file
25
netbox/dcim/migrations/0039_interface_add_enabled_mtu.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-23 17:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0038_wireless_interfaces'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mtu',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-06-23 20:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0039_interface_add_enabled_mtu'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-07-14 17:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def rpc_client_to_napalm_driver(apps, schema_editor):
|
||||
"""
|
||||
Migrate legacy RPC clients to their respective NAPALM drivers
|
||||
"""
|
||||
Platform = apps.get_model('dcim', 'Platform')
|
||||
|
||||
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
|
||||
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0040_inventoryitem_add_asset_tag_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
|
||||
),
|
||||
migrations.RunPython(rpc_client_to_napalm_driver),
|
||||
]
|
||||
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal file
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-29 21:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0041_napalm_integration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal file
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-29 21:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0042_interface_ff_10ge_cx4'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebaytemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
]
|
||||
32
netbox/dcim/migrations/0044_virtualization.py
Normal file
32
netbox/dcim/migrations/0044_virtualization.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-31 14:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0001_virtualization'),
|
||||
('dcim', '0043_device_component_name_lengths'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='virtual_machine',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
|
||||
),
|
||||
]
|
||||
20
netbox/dcim/migrations/0045_devicerole_vm_role.py
Normal file
20
netbox/dcim/migrations/0045_devicerole_vm_role.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-09-29 16:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0044_virtualization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='vm_role',
|
||||
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
|
||||
),
|
||||
]
|
||||
21
netbox/dcim/migrations/0046_rack_lengthen_facility_id.py
Normal file
21
netbox/dcim/migrations/0046_rack_lengthen_facility_id.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-09 17:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0045_devicerole_vm_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0047_more_100ge_form_factors.py
Normal file
25
netbox/dcim/migrations/0047_more_100ge_form_factors.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-09 18:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0046_rack_lengthen_facility_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
20
netbox/dcim/migrations/0048_rack_serial.py
Normal file
20
netbox/dcim/migrations/0048_rack_serial.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-09 18:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0047_more_100ge_form_factors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
]
|
||||
22
netbox/dcim/migrations/0049_rackreservation_change_user.py
Normal file
22
netbox/dcim/migrations/0049_rackreservation_change_user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-31 17:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0048_rack_serial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rackreservation',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
32
netbox/dcim/migrations/0050_interface_vlan_tagging.py
Normal file
32
netbox/dcim/migrations/0050_interface_vlan_tagging.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-10 20:10
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0020_ipaddress_add_role_carp'),
|
||||
('dcim', '0049_rackreservation_change_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='tagged_vlans',
|
||||
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='untagged_vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
|
||||
),
|
||||
]
|
||||
22
netbox/dcim/migrations/0051_rackreservation_tenant.py
Normal file
22
netbox/dcim/migrations/0051_rackreservation_tenant.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-15 18:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0003_unicode_literals'),
|
||||
('dcim', '0050_interface_vlan_tagging'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rackreservation',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
||||
44
netbox/dcim/migrations/0052_virtual_chassis.py
Normal file
44
netbox/dcim/migrations/0052_virtual_chassis.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-27 17:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0051_rackreservation_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualChassis',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('domain', models.CharField(blank=True, max_length=30)),
|
||||
('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='virtual_chassis',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='vc_priority',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set([('virtual_chassis', 'vc_position'), ('rack', 'position', 'face')]),
|
||||
),
|
||||
]
|
||||
26
netbox/dcim/migrations/0053_platform_manufacturer.py
Normal file
26
netbox/dcim/migrations/0053_platform_manufacturer.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-12-19 20:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0052_virtual_chassis'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2018-01-25 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import timezone_field.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0053_platform_manufacturer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='time_zone',
|
||||
field=timezone_field.fields.TimeZoneField(blank=True),
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
72
netbox/dcim/querysets.py
Normal file
72
netbox/dcim/querysets.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.expressions import RawSQL
|
||||
|
||||
from .constants import IFACE_ORDERING_NAME, IFACE_ORDERING_POSITION, NONCONNECTABLE_IFACE_TYPES
|
||||
|
||||
|
||||
class InterfaceQuerySet(QuerySet):
|
||||
|
||||
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||
"""
|
||||
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
|
||||
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
|
||||
would be parsed as follows:
|
||||
|
||||
name = 'GigabitEthernet'
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = 0
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
|
||||
the prescribed fields.
|
||||
"""
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: (
|
||||
'_slot', '_subslot', '_position', '_subposition', '_channel', '_type', '_vc', '_id', 'name',
|
||||
),
|
||||
IFACE_ORDERING_NAME: (
|
||||
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
|
||||
),
|
||||
}[method]
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return self.annotate(**fields).order_by(*ordering)
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
|
||||
wireless).
|
||||
"""
|
||||
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
|
||||
23
netbox/dcim/signals.py
Normal file
23
netbox/dcim/signals.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import Device, VirtualChassis
|
||||
|
||||
|
||||
@receiver(post_save, sender=VirtualChassis)
|
||||
def assign_virtualchassis_master(instance, created, **kwargs):
|
||||
"""
|
||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
||||
"""
|
||||
if created:
|
||||
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=1)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VirtualChassis)
|
||||
def clear_virtualchassis_members(instance, **kwargs):
|
||||
"""
|
||||
When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
|
||||
"""
|
||||
Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None)
|
||||
@@ -1,14 +1,34 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, Site,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
{% if record.get_children %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
||||
{% else %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}9px">
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site_list' %}?region={{ record.slug }}">{{ record.name }}</a>
|
||||
</span>
|
||||
"""
|
||||
|
||||
SITE_REGION_LINK = """
|
||||
{% if record.region %}
|
||||
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
COLOR_LABEL = """
|
||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||
@@ -20,6 +40,12 @@ DEVICE_LINK = """
|
||||
</a>
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -40,6 +66,16 @@ RACK_ROLE = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACK_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -52,6 +88,22 @@ MANUFACTURER_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
DEVICEROLE_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -62,12 +114,18 @@ DEVICE_ROLE = """
|
||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
"""
|
||||
|
||||
STATUS_ICON = """
|
||||
{% if record.status %}
|
||||
<span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-minus-sign text-danger" title="Offline" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
|
||||
DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip6.address.ip|default:"" }}
|
||||
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
||||
{{ record.primary_ip4.address.ip|default:"" }}
|
||||
"""
|
||||
|
||||
SUBDEVICE_ROLE_TEMPLATE = """
|
||||
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
@@ -75,6 +133,32 @@ UTILIZATION_GRAPH = """
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
VIRTUALCHASSIS_ACTIONS = """
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
|
||||
site_count = tables.Column(verbose_name='Sites')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
@@ -82,20 +166,14 @@ UTILIZATION_GRAPH = """
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||
name = tables.LinkColumn()
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count')
|
||||
fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -140,20 +218,30 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class RackTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices',
|
||||
'get_utilization')
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=RACK_DEVICE_COUNT,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'get_utilization',
|
||||
)
|
||||
|
||||
|
||||
class RackImportTable(BaseTable):
|
||||
@@ -161,12 +249,30 @@ class RackImportTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
u_height = tables.Column(verbose_name='Height (U)')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -177,13 +283,14 @@ class ManufacturerTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||
platform_count = tables.Column(verbose_name='Platforms')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -192,15 +299,20 @@ class ManufacturerTable(BaseTable):
|
||||
|
||||
class DeviceTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -214,7 +326,6 @@ class ConsolePortTemplateTable(BaseTable):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
@@ -224,7 +335,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
@@ -234,7 +344,6 @@ class PowerPortTemplateTable(BaseTable):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
@@ -244,17 +353,16 @@ class PowerOutletTemplateTable(BaseTable):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class InterfaceTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'form_factor')
|
||||
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
@@ -264,7 +372,6 @@ class DeviceBayTemplateTable(BaseTable):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
#
|
||||
@@ -274,15 +381,29 @@ class DeviceBayTemplateTable(BaseTable):
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_DEVICE_COUNT,
|
||||
accessor=Accessor('devices.count'),
|
||||
orderable=False,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_VM_COUNT,
|
||||
accessor=Accessor('virtual_machines.count'),
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -291,15 +412,27 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class PlatformTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=PLATFORM_DEVICE_COUNT,
|
||||
accessor=Accessor('devices.count'),
|
||||
orderable=False,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=PLATFORM_VM_COUNT,
|
||||
accessor=Accessor('virtual_machines.count'),
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -308,28 +441,37 @@ class PlatformTable(BaseTable):
|
||||
|
||||
class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
|
||||
verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name)
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
||||
template_code="{{ record.primary_ip.address.ip }}")
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||
|
||||
|
||||
class DeviceDetailTable(DeviceTable):
|
||||
primary_ip = tables.TemplateColumn(
|
||||
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
||||
)
|
||||
|
||||
class Meta(DeviceTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
|
||||
verbose_name='Site')
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
position = tables.Column(verbose_name='Position')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
@@ -337,10 +479,56 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
@@ -380,3 +568,36 @@ class InterfaceConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||
|
||||
|
||||
#
|
||||
# InventoryItems
|
||||
#
|
||||
|
||||
class InventoryItemTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
|
||||
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
master = tables.LinkColumn()
|
||||
member_count = tables.Column(verbose_name='Members')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VIRTUALCHASSIS_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user