mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-15 13:39:54 +02:00
Compare commits
677 Commits
v2.1.6
...
v2.4-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8937362433 | ||
|
|
df6c5dfac5 | ||
|
|
5cf38b5ce9 | ||
|
|
6cc9ceb198 | ||
|
|
0c0799f3bf | ||
|
|
9e2ac7b3f4 | ||
|
|
8bc8cf5e2d | ||
|
|
277197edd4 | ||
|
|
69ddf046b0 | ||
|
|
ea09023616 | ||
|
|
92de67a2ae | ||
|
|
57487f38de | ||
|
|
d334bd4477 | ||
|
|
d7e40de9da | ||
|
|
786f389be8 | ||
|
|
456b058462 | ||
|
|
ecaba5b32e | ||
|
|
9f4c77d6d7 | ||
|
|
1fb67b791f | ||
|
|
81b1d54859 | ||
|
|
67dbe02deb | ||
|
|
85efdf8e00 | ||
|
|
bbaa3a2058 | ||
|
|
931c58bc9a | ||
|
|
abd5f17916 | ||
|
|
50f4c74688 | ||
|
|
f7f7764a6e | ||
|
|
f048cf36ce | ||
|
|
a26d1812c2 | ||
|
|
b6e354085e | ||
|
|
484a74defd | ||
|
|
43ed38a6e9 | ||
|
|
0c4495eb39 | ||
|
|
864d49f54d | ||
|
|
bd2219276f | ||
|
|
df1f33992a | ||
|
|
663bbd025e | ||
|
|
4802e516e5 | ||
|
|
f2512c4fdc | ||
|
|
29172d045d | ||
|
|
289a762bf1 | ||
|
|
208409110f | ||
|
|
e27765d965 | ||
|
|
96d81d7074 | ||
|
|
edf53d4516 | ||
|
|
108e9722fa | ||
|
|
72cb1cbfff | ||
|
|
ed84c4b210 | ||
|
|
77518eaf69 | ||
|
|
4bd36f0ea9 | ||
|
|
b19bf791a4 | ||
|
|
f70b7cab21 | ||
|
|
9eb9715e05 | ||
|
|
49ecf5aa8a | ||
|
|
3ad8850ada | ||
|
|
d1c9a18d04 | ||
|
|
89e196e86d | ||
|
|
25b36d6d42 | ||
|
|
6ddbd79fe6 | ||
|
|
d70ef4d3b3 | ||
|
|
d0308e0f58 | ||
|
|
b10635a9b1 | ||
|
|
104bd1b45f | ||
|
|
302c14186a | ||
|
|
398041c607 | ||
|
|
6ce9f8f291 | ||
|
|
c2c8a139f3 | ||
|
|
698c0decb4 | ||
|
|
ef61c70a9d | ||
|
|
97863115ba | ||
|
|
fa5493a5d8 | ||
|
|
cd56e51a61 | ||
|
|
3e9cec3e8e | ||
|
|
943ec0b64b | ||
|
|
8008015082 | ||
|
|
af54d96d30 | ||
|
|
d98aa03e9d | ||
|
|
8d4c686ae2 | ||
|
|
982b9454f8 | ||
|
|
28a2a37ed2 | ||
|
|
acfbe9c1b1 | ||
|
|
4824c75563 | ||
|
|
3f019732b3 | ||
|
|
007852a48f | ||
|
|
3474697a66 | ||
|
|
bf1c7cacc6 | ||
|
|
b9bdd666da | ||
|
|
35d58d2f7c | ||
|
|
f5f16ce64b | ||
|
|
06dab9c468 | ||
|
|
7857480978 | ||
|
|
278bacbce8 | ||
|
|
743cf6d398 | ||
|
|
ace7e3b108 | ||
|
|
1edc73179a | ||
|
|
65dd7a5938 | ||
|
|
62989ecb6e | ||
|
|
b952ec73ce | ||
|
|
65e18e057f | ||
|
|
c13e4858d7 | ||
|
|
4e09b32dd9 | ||
|
|
ffcbc54522 | ||
|
|
06143b6c70 | ||
|
|
0af36eb99b | ||
|
|
b11c3635b0 | ||
|
|
66c4911298 | ||
|
|
36971b7651 | ||
|
|
3bdfe9c249 | ||
|
|
4e6f73e452 | ||
|
|
ce27a1d211 | ||
|
|
2d198403c7 | ||
|
|
6c1b5fdf3a | ||
|
|
9d419de9dc | ||
|
|
b945dec41b | ||
|
|
7819d9c112 | ||
|
|
258373f1a1 | ||
|
|
e1055b7f97 | ||
|
|
a1f6ed1713 | ||
|
|
4ffce75b70 | ||
|
|
09212691e2 | ||
|
|
ddd878683d | ||
|
|
a8b11e45c1 | ||
|
|
23f91274d6 | ||
|
|
6dde0f030a | ||
|
|
d154b4cc9e | ||
|
|
7c11fa7b50 | ||
|
|
264bf6c484 | ||
|
|
3854a9d633 | ||
|
|
38569029d8 | ||
|
|
3c2e0b0b17 | ||
|
|
21c4085c51 | ||
|
|
33cf227bc8 | ||
|
|
b556d2d626 | ||
|
|
81258ea35b | ||
|
|
90abeedc3e | ||
|
|
048e843c39 | ||
|
|
e4f336a843 | ||
|
|
33add12069 | ||
|
|
8bad3aee74 | ||
|
|
ff0a0df478 | ||
|
|
5dd2f37035 | ||
|
|
862e44e96f | ||
|
|
643b0eaf65 | ||
|
|
0af6df3121 | ||
|
|
e0616d933f | ||
|
|
1e7fdbc79a | ||
|
|
5591107f95 | ||
|
|
e3c3e54cbb | ||
|
|
75525cc83f | ||
|
|
ff1217fca9 | ||
|
|
a61473dd98 | ||
|
|
edd8e9e41e | ||
|
|
efa118c3c8 | ||
|
|
503efe2d9d | ||
|
|
8762f1314d | ||
|
|
836478c166 | ||
|
|
acc59a9da5 | ||
|
|
03ce4bdfca | ||
|
|
1473d90243 | ||
|
|
32eee0bede | ||
|
|
131436fc20 | ||
|
|
966c188977 | ||
|
|
afba80bff9 | ||
|
|
4fd52d46bf | ||
|
|
8f9fc8fb51 | ||
|
|
b0985ebd42 | ||
|
|
0d267d97fe | ||
|
|
b0cd372af9 | ||
|
|
63100b683d | ||
|
|
74aa992ec6 | ||
|
|
dc2f1d7c64 | ||
|
|
03a1c48b54 | ||
|
|
918339cfa8 | ||
|
|
601fb418b5 | ||
|
|
e5af4f6f17 | ||
|
|
399a633d9d | ||
|
|
2ef223b5ea | ||
|
|
2cdb527df9 | ||
|
|
b3350490e7 | ||
|
|
1d1553275e | ||
|
|
0189609137 | ||
|
|
e6b3983a4e | ||
|
|
5247f10d7e | ||
|
|
9b3869790d | ||
|
|
b0dafcf50f | ||
|
|
fc0e8e2aae | ||
|
|
57f6d22c64 | ||
|
|
e5454d6714 | ||
|
|
a7389de109 | ||
|
|
b911ab01d2 | ||
|
|
9153c71cbf | ||
|
|
b44aa9d32e | ||
|
|
7805848e6c | ||
|
|
bcb1d9af0b | ||
|
|
ef84889a57 | ||
|
|
81c027e7cf | ||
|
|
fd62a248ee | ||
|
|
2c8bea1b59 | ||
|
|
aeaa47e91d | ||
|
|
9de1a8c363 | ||
|
|
c72d70d114 | ||
|
|
821fb1e01e | ||
|
|
7241783249 | ||
|
|
db3cbaf83b | ||
|
|
72c518bcb7 | ||
|
|
9725f19bae | ||
|
|
0bb632c642 | ||
|
|
0969c458b3 | ||
|
|
07364abf9e | ||
|
|
20cb13e1bb | ||
|
|
3f3b385de7 | ||
|
|
94b12e506e | ||
|
|
4ec6e52e73 | ||
|
|
88adc5ca86 | ||
|
|
223c95adbc | ||
|
|
3aaca1ca02 | ||
|
|
6a4d17b8a5 | ||
|
|
720c5fabaf | ||
|
|
1c5239a4d0 | ||
|
|
05b5609d86 | ||
|
|
7e92aeb7ac | ||
|
|
6e2eb15a80 | ||
|
|
0b825ac3d0 | ||
|
|
b5f1d74d6f | ||
|
|
e071b7dfd5 | ||
|
|
53e4e74930 | ||
|
|
b83de7eb11 | ||
|
|
38a208242b | ||
|
|
4acd8e180d | ||
|
|
debc8521a5 | ||
|
|
8bd268d81c | ||
|
|
ae6848b194 | ||
|
|
b22744b031 | ||
|
|
a75d7079df | ||
|
|
aa8442a345 | ||
|
|
70625a5cb0 | ||
|
|
7c043d9b4f | ||
|
|
546f17ab50 | ||
|
|
1c9986efc4 | ||
|
|
8ae13e29f5 | ||
|
|
f5bb072f28 | ||
|
|
37eef0ba6d | ||
|
|
603b80db1b | ||
|
|
8d9543cb6a | ||
|
|
c823660a8f | ||
|
|
0c5ad85b35 | ||
|
|
bdecf7a3e3 | ||
|
|
6b62720daf | ||
|
|
d48c450018 | ||
|
|
078404fb59 | ||
|
|
4bb526896f | ||
|
|
0476006ef2 | ||
|
|
19831f0177 | ||
|
|
fc9871fba3 | ||
|
|
b34f4f8e43 | ||
|
|
0357d8522c | ||
|
|
08d06bd781 | ||
|
|
01a97add2a | ||
|
|
3cb351dceb | ||
|
|
9e11591b3b | ||
|
|
e4c1cece75 | ||
|
|
6881a98048 | ||
|
|
36de9f10d6 | ||
|
|
1cc135f01f | ||
|
|
079c8894fa | ||
|
|
970759ed8b | ||
|
|
22f17a1424 | ||
|
|
5ed797cfc9 | ||
|
|
8ad59058a5 | ||
|
|
ec7bbcf90d | ||
|
|
37dde72c8f | ||
|
|
972f9be291 | ||
|
|
8b33b888b2 | ||
|
|
d29fd338eb | ||
|
|
8b5dba25f5 | ||
|
|
e18b5f5fd4 | ||
|
|
a5dc9537e5 | ||
|
|
3064948d8c | ||
|
|
e6bcc4a3fe | ||
|
|
6967b6bdc5 | ||
|
|
a8977a5dec | ||
|
|
b837e8ea0b | ||
|
|
110052fa0f | ||
|
|
84bb977d2e | ||
|
|
2d93c2b2da | ||
|
|
9e4f2a9614 | ||
|
|
5412a9f8ea | ||
|
|
d7177d3e05 | ||
|
|
a21bd81681 | ||
|
|
e653f35bf1 | ||
|
|
28ea06a8bc | ||
|
|
86b0491b68 | ||
|
|
c8309581be | ||
|
|
376c531fe4 | ||
|
|
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 | ||
|
|
57973f62c5 | ||
|
|
e56fc4b1ee | ||
|
|
76ebd2d34f | ||
|
|
85c273c8ca | ||
|
|
b9cd834e95 | ||
|
|
cfa6bee081 | ||
|
|
b46cc2c1a9 | ||
|
|
5e734fc5a6 | ||
|
|
d08bc7767e | ||
|
|
bbd0761887 | ||
|
|
4668149943 | ||
|
|
a5b7c057eb | ||
|
|
0a04bb110a | ||
|
|
9c0b414676 | ||
|
|
5be4b0c4fd | ||
|
|
8e0eab20e2 | ||
|
|
1b5aa67f5d | ||
|
|
02c278f393 | ||
|
|
e57b8aa26f | ||
|
|
3d023126ba | ||
|
|
53f58d4496 | ||
|
|
1a6ee237f6 | ||
|
|
33a99441a4 | ||
|
|
3df7e283e3 | ||
|
|
b295849f53 | ||
|
|
c107f35118 | ||
|
|
3d91153275 | ||
|
|
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 | ||
|
|
baa277c592 | ||
|
|
007f930fed | ||
|
|
f253f164a3 | ||
|
|
abfe71bb04 | ||
|
|
0c645b12d1 | ||
|
|
f824d1eb3b | ||
|
|
afbbe1148f | ||
|
|
8403e91fc9 | ||
|
|
c300879995 | ||
|
|
ea1d298eb3 | ||
|
|
d17b36519a | ||
|
|
f4f0896470 | ||
|
|
2c9b41db75 | ||
|
|
a38cd449c5 | ||
|
|
977cad3830 | ||
|
|
987587b5f3 | ||
|
|
9351f686b1 | ||
|
|
42e557bd3f | ||
|
|
35c2c8e8de | ||
|
|
960e1d49c2 | ||
|
|
100d979d3e | ||
|
|
b734599f40 | ||
|
|
b41b4fc584 | ||
|
|
98195c9e74 | ||
|
|
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 | ||
|
|
2ca161f3d8 | ||
|
|
79fdf641c0 | ||
|
|
b5ab498e75 | ||
|
|
16d1f9aca8 | ||
|
|
4cfad2ef3b | ||
|
|
6e81b0ac32 | ||
|
|
d87023e1ce | ||
|
|
2174ccf016 | ||
|
|
8f1607e010 | ||
|
|
e93129f1ae | ||
|
|
35b4e190d9 | ||
|
|
512a3bf09a | ||
|
|
9927ce14d3 | ||
|
|
e5e169f476 | ||
|
|
5fc92ab0e6 | ||
|
|
039c2a6d57 | ||
|
|
d053422657 | ||
|
|
90d9eb3520 | ||
|
|
3466da4338 | ||
|
|
6872ab0e14 | ||
|
|
136d16b7fd | ||
|
|
d651deb31c | ||
|
|
8fb37233f4 | ||
|
|
700194b80d | ||
|
|
ef2dd673ec | ||
|
|
7a3adca771 | ||
|
|
f66b0b7fea | ||
|
|
00024240bb | ||
|
|
237a889f54 | ||
|
|
f5c265b7b5 | ||
|
|
1341ab5703 | ||
|
|
3ded8196c4 | ||
|
|
cbc239ceaa | ||
|
|
a6b43baafe | ||
|
|
ba0a261840 | ||
|
|
3bb0d523d3 | ||
|
|
85cb333a5d | ||
|
|
2abb073b3a | ||
|
|
0634386b2e | ||
|
|
71069ced7f | ||
|
|
fa95191792 | ||
|
|
e945aafd7b | ||
|
|
e9f75adddb | ||
|
|
6ebd8e78c8 | ||
|
|
daa947e7eb | ||
|
|
b117130aac | ||
|
|
4587aba1d4 | ||
|
|
5930a64203 | ||
|
|
97536c4e9b | ||
|
|
e81e33af38 | ||
|
|
a7c56eab86 | ||
|
|
6e580a731e | ||
|
|
a02055e9b5 | ||
|
|
9acd792abe | ||
|
|
4ef55502b4 | ||
|
|
3009863877 | ||
|
|
b67e3ff957 | ||
|
|
d06813f528 | ||
|
|
4544893b4c | ||
|
|
f30fa925ea | ||
|
|
beb91559e5 | ||
|
|
fbc5e74318 |
28
.github/ISSUE_TEMPLATE.md
vendored
28
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,32 +4,40 @@
|
|||||||
remove the "is:open" filter from the search bar to include closed issues.
|
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
|
Check the appropriate type for your issue below by placing an x between the
|
||||||
brackets. If none of the below apply, please raise your issue for
|
brackets. For assistance with installation issues, or for any other issues
|
||||||
discussion on our mailing list:
|
other than those listed below, please raise your topic for discussion on
|
||||||
|
our mailing list:
|
||||||
|
|
||||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||||
|
|
||||||
Please note that issues which do not fall under any of the below categories
|
Please note that issues which do not fall under any of the below categories
|
||||||
will be closed.
|
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
|
### Issue type
|
||||||
[ ] Feature request <!-- Requesting the implementation of a new feature -->
|
[ ] Feature request <!-- An enhancement of existing functionality -->
|
||||||
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
|
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
||||||
[ ] Documentation <!-- Proposing a modification to the documentation -->
|
[ ] Documentation <!-- A modification to the documentation -->
|
||||||
|
[ ] Housekeeping <!-- Changes pertaining to the codebase itself -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please describe the environment in which you are running NetBox. (Be sure
|
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
|
to verify that you are running the latest stable release of NetBox before
|
||||||
submitting a bug report.)
|
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
|
### Environment
|
||||||
* Python version: <!-- Example: 3.5.4 -->
|
* Python version: <!-- Example: 3.5.4 -->
|
||||||
* NetBox version: <!-- Example: 2.1.3 -->
|
* NetBox version: <!-- Example: 2.3.5 -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
BUG REPORTS must include:
|
BUG REPORTS must include:
|
||||||
* A list of the steps needed to reproduce the bug
|
* A list of the steps needed for someone else to reproduce the bug
|
||||||
* A description of the expected behavior
|
* A description of the expected and observed behavior
|
||||||
* Any relevant error messages (screenshots may also help)
|
* Any relevant error messages (screenshots may also help)
|
||||||
|
|
||||||
FEATURE REQUESTS must include:
|
FEATURE REQUESTS must include:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
/netbox/netbox/ldap_config.py
|
/netbox/netbox/ldap_config.py
|
||||||
|
/netbox/reports/*
|
||||||
|
!/netbox/reports/__init__.py
|
||||||
/netbox/static
|
/netbox/static
|
||||||
.idea
|
.idea
|
||||||
/*.sh
|
/*.sh
|
||||||
|
|||||||
18
.travis.yml
18
.travis.yml
@@ -1,21 +1,17 @@
|
|||||||
sudo: required
|
sudo: required
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- docker
|
- postgresql
|
||||||
|
addons:
|
||||||
env:
|
postgresql: "9.4"
|
||||||
- DOCKER_TAG=$TRAVIS_TAG
|
|
||||||
|
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install pep8
|
- pip install pycodestyle
|
||||||
|
before_script:
|
||||||
|
- psql --version
|
||||||
|
- psql -U postgres -c 'SELECT version();'
|
||||||
script:
|
script:
|
||||||
- ./scripts/cibuild.sh
|
- ./scripts/cibuild.sh
|
||||||
after_success:
|
|
||||||
- if [ ! -z "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
|
|
||||||
./scripts/docker-build.sh;
|
|
||||||
fi
|
|
||||||
|
|||||||
138
CONTRIBUTING.md
138
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
|||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you encounter any issues installing or using NetBox, try one of the
|
If you encounter any issues installing or using NetBox, try one of the
|
||||||
following resources to get assistance. Please **do not** open a GitHub
|
following resources to get assistance. Please **do not** open a GitHub issue
|
||||||
issue except to report bugs or request features.
|
except to report bugs or request features.
|
||||||
|
|
||||||
### Mailing List
|
### Mailing List
|
||||||
|
|
||||||
@@ -10,112 +10,108 @@ We have established a Google Groups Mailing List for issues and general
|
|||||||
discussion. This is the best forum for obtaining assistance with NetBox
|
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).
|
installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
|
||||||
|
|
||||||
### Freenode IRC
|
### Slack
|
||||||
|
|
||||||
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
|
For real-time discussion, you can join the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/).
|
||||||
You can connect to Freenode at irc.freenode.net using an IRC client, or
|
|
||||||
you can use their [webchat client](https://webchat.freenode.net/).
|
|
||||||
|
|
||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
|
||||||
NetBox. If you're running an older version, it's possible that the bug
|
of NetBox. If you're running an older version, it's possible that the bug has
|
||||||
has already been fixed.
|
already been fixed.
|
||||||
|
|
||||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
|
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||||
already been reported. If you think you may be experiencing a reported
|
to see if the bug you've found has already been reported. If you think you may
|
||||||
issue that hasn't already been resolved, please click "add a reaction"
|
be experiencing a reported issue that hasn't already been resolved, please
|
||||||
in the top right corner of the issue and add a thumbs up (+1). You might
|
click "add a reaction" in the top right corner of the issue and add a thumbs
|
||||||
also want to add a comment describing how it's affecting your
|
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
|
installation. This will allow us to prioritize bugs based on how many users are
|
||||||
users are affected.
|
affected.
|
||||||
|
|
||||||
* If you haven't found an existing issue that describes your suspected
|
* If you haven't found an existing issue that describes your suspected bug,
|
||||||
bug, please inquire about it on the mailing list. **Do not** file an
|
please inquire about it on the mailing list. **Do not** file an issue until you
|
||||||
issue until you have received confirmation that it is in fact a bug.
|
have received confirmation that it is in fact a bug. Invalid issues are very
|
||||||
Invalid issues are very distracting and slow the pace at which NetBox is
|
distracting and slow the pace at which NetBox is developed.
|
||||||
developed.
|
|
||||||
|
|
||||||
* When submitting an issue, please be as descriptive as possible. Be
|
* When submitting an issue, please be as descriptive as possible. Be sure to
|
||||||
sure to include:
|
include:
|
||||||
|
|
||||||
* The environment in which NetBox is running
|
* The environment in which NetBox is running
|
||||||
* The exact steps that can be taken to reproduce the issue (if
|
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||||
applicable)
|
|
||||||
* Any error messages generated
|
* Any error messages generated
|
||||||
* Screenshots (if applicable)
|
* Screenshots (if applicable)
|
||||||
|
|
||||||
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
|
* 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
|
The issue will be reviewed by a moderator after submission and the appropriate
|
||||||
labels will be applied.
|
labels will be applied for categorization.
|
||||||
|
|
||||||
* Keep in mind that we prioritize bugs based on their severity and how
|
* Keep in mind that we prioritize bugs based on their severity and how much
|
||||||
much work is required to resolve them. It may take some time for someone
|
work is required to resolve them. It may take some time for someone to address
|
||||||
to address your issue.
|
your issue.
|
||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||||
requesting is already listed. (Be sure to search closed issues as well,
|
to see if the feature you're requesting is already listed. (Be sure to search
|
||||||
since some feature requests are rejected.) If the feature you'd like to
|
closed issues as well, since some feature requests have been rejected.) If the
|
||||||
see has already been requested, click "add a reaction" in the top right
|
feature you'd like to see has already been requested and is open, click "add a
|
||||||
corner of the issue and add a thumbs up (+1). This ensures that the
|
reaction" in the top right corner of the issue and add a thumbs up (+1). This
|
||||||
issue has a better chance of making it onto the roadmap. Also feel free
|
ensures that the issue has a better chance of receiving attention. Also feel
|
||||||
to add a comment with any additional justification for the feature.
|
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
|
(However, note that comments with no substance other than a "+1" will be
|
||||||
deleted. Please use GitHub's reactions feature to indicate your
|
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||||
support.)
|
|
||||||
|
|
||||||
* While suggestions for new features are welcome, it's important to
|
* Due to an excessive backlog of feature requests, we are not currently
|
||||||
limit the scope of NetBox's feature set to avoid feature creep. For
|
accepting any proposals which substantially extend NetBox's functionality
|
||||||
example, the following features would be firmly out of scope for NetBox:
|
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
|
* Before filing a new feature request, consider raising your idea on the
|
||||||
mailing list first. Feedback you receive there will help validate and
|
mailing list first. Feedback you receive there will help validate and shape the
|
||||||
shape the proposed feature before filing a formal issue.
|
proposed feature before filing a formal issue.
|
||||||
|
|
||||||
* Good feature requests are very narrowly defined. Be sure to enumerate
|
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||||
specific functionality and data schema. The more effort you put into
|
describe the functionality and data model(s) being proposed. The more effort
|
||||||
writing a feature request, the better its chance is of being
|
you put into writing a feature request, the better its chance is of being
|
||||||
implemented. Overly broad feature requests will be closed.
|
implemented. Overly broad feature requests will be closed.
|
||||||
|
|
||||||
* When submitting a feature request on GitHub, be sure to include the
|
* When submitting a feature request on GitHub, be sure to include the
|
||||||
following:
|
following:
|
||||||
|
|
||||||
* A detailed description of the proposed functionality
|
* A detailed description of the proposed functionality
|
||||||
* A use case for the feature; who would use it and what value it
|
* A use case for the feature; who would use it and what value it would add
|
||||||
would add to NetBox
|
to NetBox
|
||||||
* A rough description of changes necessary to the database schema
|
* A rough description of changes necessary to the database schema (if
|
||||||
(if applicable)
|
applicable)
|
||||||
* Any third-party libraries or other resources which would be
|
* Any third-party libraries or other resources which would be involved
|
||||||
involved
|
|
||||||
|
|
||||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
|
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
|
||||||
The issue will be reviewed by a moderator after submission and the appropriate
|
title. The issue will be reviewed by a moderator after submission and the
|
||||||
labels will be applied.
|
appropriate labels will be applied for categorization.
|
||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
* Be sure to open an issue before starting work on a pull request, and
|
* Be sure to open an issue before starting work on a pull request, and discuss
|
||||||
discuss your idea with the NetBox maintainers before beginning work.
|
your idea with the NetBox maintainers before beginning work. This will help
|
||||||
This will help prevent wasting time on something that might we might not
|
prevent wasting time on something that might we might not be able to implement.
|
||||||
be able to implement. When suggesting a new feature, also make sure it
|
When suggesting a new feature, also make sure it won't conflict with any work
|
||||||
won't conflict with any work that's already in progress.
|
that's already in progress.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of the
|
* When submitting a pull request, please be sure to work off of the `develop`
|
||||||
`develop` branch, rather than `master`. In NetBox, the `develop` branch
|
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||||
is used for ongoing development, while `master` is used for tagging new
|
development, while `master` is used for tagging new stable releases.
|
||||||
stable releases.
|
|
||||||
|
|
||||||
* All code submissions should meet the following criteria (CI will
|
* All code submissions should meet the following criteria (CI will enforce
|
||||||
enforce these checks):
|
these checks):
|
||||||
|
|
||||||
* Python syntax is valid
|
* Python syntax is valid
|
||||||
* All tests pass when run with `./manage.py test`
|
* All tests pass when run with `./manage.py test`
|
||||||
* PEP 8 compliance is enforced, with the exception that lines may be
|
* PEP 8 compliance is enforced, with the exception that lines may be
|
||||||
greater than 80 characters in length
|
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.
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -1,16 +1,22 @@
|
|||||||

|

|
||||||
|
|
||||||
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/).
|
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://networktocode.slack.com/)!
|
||||||
|
|
||||||
### Build Status
|
### Build Status
|
||||||
|
|
||||||
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongly recommended.
|
||||||
|
|
||||||
| | status |
|
| | status |
|
||||||
|-------------|------------|
|
|-------------|------------|
|
||||||
@@ -27,10 +33,12 @@ NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
|||||||
|
|
||||||
# Installation
|
# 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
|
## Alternative Installations
|
||||||
|
|
||||||
* [Docker container](https://github.com/digitalocean/netbox-docker)
|
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||||
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
|
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
|
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
||||||
|
|||||||
21
base_requirements.txt
Normal file
21
base_requirements.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Django
|
||||||
|
django-cors-headers
|
||||||
|
django-debug-toolbar
|
||||||
|
django-filter==1.1.0
|
||||||
|
django-mptt
|
||||||
|
django-tables2
|
||||||
|
django-taggit
|
||||||
|
django-timezone-field
|
||||||
|
djangorestframework==3.8.1
|
||||||
|
drf-yasg[validation]
|
||||||
|
graphviz
|
||||||
|
Markdown
|
||||||
|
natsort
|
||||||
|
ncclient
|
||||||
|
netaddr
|
||||||
|
paramiko
|
||||||
|
Pillow
|
||||||
|
psycopg2-binary
|
||||||
|
py-gfm
|
||||||
|
pycryptodome
|
||||||
|
xmltodict
|
||||||
@@ -5,7 +5,7 @@ Supported HTTP methods:
|
|||||||
* `GET`: Retrieve an object or list of objects
|
* `GET`: Retrieve an object or list of objects
|
||||||
* `POST`: Create a new object
|
* `POST`: Create a new object
|
||||||
* `PUT`: Update an existing object, all mandatory fields must be specified
|
* `PUT`: Update an existing object, all mandatory fields must be specified
|
||||||
* `PATCH`: Updates an existing object, only specifiying the field to be changed
|
* `PATCH`: Updates an existing object, only specifying the field to be changed
|
||||||
* `DELETE`: Delete an existing object
|
* `DELETE`: Delete an existing object
|
||||||
|
|
||||||
To authenticate a request, attach your token in an `Authorization` header:
|
To authenticate a request, attach your token in an `Authorization` header:
|
||||||
@@ -82,15 +82,15 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6
|
|||||||
|
|
||||||
### Creating a new site
|
### 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.
|
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"}'
|
$ 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,
|
"id": 16,
|
||||||
"name": "My New Site",
|
"name": "My New Site",
|
||||||
"slug": "my-new-site",
|
"slug": "my-new-site",
|
||||||
"region": null,
|
"region": 5,
|
||||||
"tenant": null,
|
"tenant": null,
|
||||||
"facility": "",
|
"facility": "",
|
||||||
"asn": null,
|
"asn": null,
|
||||||
@@ -102,6 +102,7 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
|
|||||||
"comments": ""
|
"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
|
### Modify an existing site
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
|
|||||||
Send an authenticated `DELETE` request to the site detail endpoint.
|
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/
|
$ 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)
|
* Connected to localhost (127.0.0.1) port 8000 (#0)
|
||||||
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
||||||
> User-Agent: curl/7.35.0
|
> User-Agent: curl/7.35.0
|
||||||
@@ -143,4 +144,4 @@ $ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9
|
|||||||
* Closing connection 0
|
* Closing connection 0
|
||||||
```
|
```
|
||||||
|
|
||||||
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.
|
The response to a successful `DELETE` request will have code 204 (No Content); the body of the response will be empty.
|
||||||
|
|||||||
@@ -88,10 +88,10 @@ The base serializer is used to represent the default view of a model. This inclu
|
|||||||
"vid": 101,
|
"vid": 101,
|
||||||
"name": "Users-Floor1",
|
"name": "Users-Floor1",
|
||||||
"tenant": null,
|
"tenant": null,
|
||||||
"status": [
|
"status": {
|
||||||
1,
|
"value": 1,
|
||||||
"Active"
|
"label": "Active"
|
||||||
],
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"url": "http://localhost:8000/api/ipam/roles/9/",
|
"url": "http://localhost:8000/api/ipam/roles/9/",
|
||||||
@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -122,6 +122,37 @@ When a base serializer includes one or more nested serializers, the hierarchical
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
# 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:
|
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:
|
||||||
@@ -175,3 +206,28 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_
|
|||||||
|
|
||||||
!!! warning
|
!!! 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.
|
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.
|
||||||
|
|
||||||
|
# Filtering
|
||||||
|
|
||||||
|
A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`):
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/ipam/prefixes/?status=1
|
||||||
|
```
|
||||||
|
|
||||||
|
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/ipam/prefixes/?status=1&status=2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Fields
|
||||||
|
|
||||||
|
To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dcim/sites/?cf_foo=123
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Full versus partial matching when filtering is configurable per custom field. Filtering can be toggled (or disabled) for a custom field in the admin UI.
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ BASE_PATH = 'netbox/'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## CHANGELOG_RETENTION
|
||||||
|
|
||||||
|
Default: 90
|
||||||
|
|
||||||
|
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain changes in the database indefinitely. (Warning: This will greatly increase database size over time.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CORS_ORIGIN_ALLOW_ALL
|
## CORS_ORIGIN_ALLOW_ALL
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
@@ -145,7 +153,7 @@ An API consumer can request an arbitrary number of objects by appending the "lim
|
|||||||
|
|
||||||
Default: $BASE_DIR/netbox/media/
|
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -191,14 +199,6 @@ The amount of time (in seconds) to wait for NAPALM to connect to a device.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## NETBOX_USERNAME (Deprecated)
|
|
||||||
|
|
||||||
## NETBOX_PASSWORD (Deprecated)
|
|
||||||
|
|
||||||
These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PAGINATE_COUNT
|
## PAGINATE_COUNT
|
||||||
|
|
||||||
Default: 50
|
Default: 50
|
||||||
@@ -215,6 +215,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
|
## TIME_ZONE
|
||||||
|
|
||||||
Default: UTC
|
Default: UTC
|
||||||
@@ -223,6 +231,14 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## WEBHOOKS_ENABLED
|
||||||
|
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../miscellaneous/webhooks/) for more information on setup and use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Date and Time Formatting
|
## Date and Time Formatting
|
||||||
|
|
||||||
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date).
|
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date).
|
||||||
@@ -237,3 +253,49 @@ SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00
|
|||||||
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
|
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
|
||||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
|
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redis Connection Settings
|
||||||
|
|
||||||
|
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../miscellaneous/webhooks/). A Redis connection is configured using a dictionary similar to the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
REDIS = {
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': 6379,
|
||||||
|
'PASSWORD': '',
|
||||||
|
'DATABASE': 0,
|
||||||
|
'DEFAULT_TIMEOUT': 300,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DATABASE
|
||||||
|
|
||||||
|
Default: 0
|
||||||
|
|
||||||
|
The Redis database ID.
|
||||||
|
|
||||||
|
### DEFAULT_TIMEOUT
|
||||||
|
|
||||||
|
Default: 300
|
||||||
|
|
||||||
|
The timeout value to use when connecting to the Redis server (in seconds).
|
||||||
|
|
||||||
|
### HOST
|
||||||
|
|
||||||
|
Default: localhost
|
||||||
|
|
||||||
|
The hostname or IP address of the Redis server.
|
||||||
|
|
||||||
|
### PORT
|
||||||
|
|
||||||
|
Default: 6379
|
||||||
|
|
||||||
|
The TCP port to use when connecting to the Redis server.
|
||||||
|
|
||||||
|
### PASSWORD
|
||||||
|
|
||||||
|
Default: None
|
||||||
|
|
||||||
|
The password to use when authenticating to the Redis server (optional).
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ A device type represents a particular hardware model that exists in the real wor
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
A device type can be a parent, child, or neither. Parent devices house child devices in device bays. This relationship is used to model things like blade servers, where child devices function independently but share physical resources like rack space and power. Note that this is **not** intended to model chassis-based devices, wherein child members share a common control plane.
|
||||||
|
|
||||||
### Manufacturers
|
### Manufacturers
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -112,3 +114,11 @@ Console ports connect only to console server ports, and power ports connect only
|
|||||||
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.
|
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.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
|
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
|
||||||
|
Tags are freeform labels which can be assigned to a variety of objects in NetBox. Tags can be used to categorize and filter objects in addition to built-in and custom fields. Each tag consists of a text label, as well as an auto-generated URL-friendly slug value. Objects can be filtered by the tags assigned to them. Tags can be used across different object types.
|
||||||
|
|
||||||
# Custom Fields
|
# 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.
|
||||||
@@ -27,6 +31,10 @@ When a single object is edited, the form will include any custom fields which ha
|
|||||||
|
|
||||||
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
||||||
|
|
||||||
|
# Contextual Configuration Data
|
||||||
|
|
||||||
|
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. (For example, you might want to associate a set of syslog servers for all devices at a particular site.) Context data enables the association of arbitrary data (expressed in JSON format) to devices and virtual machines grouped by region, site, role, platform, and/or tenancy. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
||||||
|
|
||||||
# Export Templates
|
# Export Templates
|
||||||
|
|
||||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
||||||
@@ -130,3 +138,61 @@ Certain objects within NetBox (namely sites, racks, and devices) can have photos
|
|||||||
|
|
||||||
!!! note
|
!!! 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/`).
|
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/`).
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
|
||||||
|
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks.
|
||||||
|
|
||||||
|
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
{
|
||||||
|
"event": "created",
|
||||||
|
"signal_received_timestamp": 1508769597,
|
||||||
|
"model": "Site"
|
||||||
|
"data": {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
{
|
||||||
|
"event": "deleted",
|
||||||
|
"signal_received_timestamp": 1508781858.544069,
|
||||||
|
"model": "Site",
|
||||||
|
"data": {
|
||||||
|
"asn": None,
|
||||||
|
"comments": "",
|
||||||
|
"contact_email": "",
|
||||||
|
"contact_name": "",
|
||||||
|
"contact_phone": "",
|
||||||
|
"count_circuits": 0,
|
||||||
|
"count_devices": 0,
|
||||||
|
"count_prefixes": 0,
|
||||||
|
"count_racks": 0,
|
||||||
|
"count_vlans": 0,
|
||||||
|
"custom_fields": {},
|
||||||
|
"facility": "",
|
||||||
|
"id": 54,
|
||||||
|
"name": "test",
|
||||||
|
"physical_address": "",
|
||||||
|
"region": None,
|
||||||
|
"shipping_address": "",
|
||||||
|
"slug": "test",
|
||||||
|
"tenant": None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
|
||||||
|
|
||||||
|
## Backend Status
|
||||||
|
|
||||||
|
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.
|
||||||
|
|||||||
@@ -96,4 +96,4 @@ VLAN groups can be employed for administrative organization within NetBox. Each
|
|||||||
|
|
||||||
# Services
|
# 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 assigned 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.)
|
||||||
|
|||||||
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
|
* **Equipment racks** - Organized by group and site
|
||||||
* **Devices** - Types of devices and where they are installed
|
* **Devices** - Types of devices and where they are installed
|
||||||
* **Connections** - Network, console, and power connections among devices
|
* **Connections** - Network, console, and power connections among devices
|
||||||
|
* **Virtualization** - Virtual machines and clusters
|
||||||
* **Data circuits** - Long-haul communications circuits and providers
|
* **Data circuits** - Long-haul communications circuits and providers
|
||||||
* **Secrets** - Encrypted storage of sensitive credentials
|
* **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 |
|
| HTTP Service | nginx or Apache |
|
||||||
| WSGI Service | gunicorn or uWSGI |
|
| WSGI Service | gunicorn or uWSGI |
|
||||||
| Application | Django/Python |
|
| Application | Django/Python |
|
||||||
| Database | PostgreSQL |
|
| Database | PostgreSQL 9.4+ |
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ This guide explains how to implement LDAP authentication using an external serve
|
|||||||
On Ubuntu:
|
On Ubuntu:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
|
sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
On CentOS:
|
On CentOS:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo yum install -y python-devel openldap-devel
|
sudo yum install -y openldap-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install django-auth-ldap
|
## Install django-auth-ldap
|
||||||
@@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
|
|||||||
|
|
||||||
# Configuration
|
# 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
|
## General Server Configuration
|
||||||
|
|
||||||
@@ -52,10 +52,12 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
|||||||
LDAP_IGNORE_CERT_ERRORS = True
|
LDAP_IGNORE_CERT_ERRORS = True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||||
|
|
||||||
## User Authentication
|
## User Authentication
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch
|
from django_auth_ldap.config import LDAPSearch
|
||||||
@@ -78,14 +80,14 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# User Groups for Permissions
|
# User Groups for Permissions
|
||||||
!!! Info
|
!!! info
|
||||||
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for AUTH_LDAP_GROUP_TYPE.
|
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||||
|
|
||||||
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
|
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
|
||||||
# heirarchy.
|
# hierarchy.
|
||||||
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE,
|
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE,
|
||||||
"(objectClass=group)")
|
"(objectClass=group)")
|
||||||
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
|
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Migration
|
# Migration
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Beginning with v2.5, NetBox will no longer support Python 2. It is strongly recommended that you upgrade to Python 3 as soon as possible.
|
||||||
|
|
||||||
Remove Python 2 packages
|
Remove Python 2 packages
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
|
|||||||
@@ -1,35 +1,20 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
This section of the documentation discusses installing and configuring the NetBox application.
|
||||||
|
|
||||||
**Ubuntu**
|
**Ubuntu**
|
||||||
|
|
||||||
Python 3:
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-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 zlib1g-dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**CentOS**
|
**CentOS**
|
||||||
|
|
||||||
Python 3:
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# yum install -y epel-release
|
# yum install -y epel-release
|
||||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
# 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
|
# easy_install-3.4 pip
|
||||||
# ln -s -f python3.4 /usr/bin/python
|
|
||||||
```
|
|
||||||
|
|
||||||
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 redhat-rpm-config
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
||||||
@@ -81,30 +66,52 @@ Resolving deltas: 100% (1495/1495), done.
|
|||||||
Checking connectivity... 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 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.)
|
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
|
```no-highlight
|
||||||
# pip3 install -r requirements.txt
|
# pip3 install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Python 2:
|
!!! 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 `pip3 -V`.
|
||||||
|
|
||||||
```no-highlight
|
### NAPALM Automation (Optional)
|
||||||
# pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### NAPALM Automation
|
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:
|
||||||
|
|
||||||
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
|
```no-highlight
|
||||||
# pip3 install napalm
|
# pip3 install napalm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Webhooks (Optional)
|
||||||
|
|
||||||
|
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
|
||||||
|
|
||||||
|
**Ubuntu**
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# apt-get install -y redis-server
|
||||||
|
```
|
||||||
|
|
||||||
|
**CentOS**
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# yum install -y redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Enabling webhooks also requires installing the [`django-rq`](https://github.com/ui/django-rq) package. This allows NetBox to use the Redis database as a queue for outgoing webhooks.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# pip3 install django-rq
|
||||||
|
```
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
||||||
@@ -155,16 +162,28 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
|
|||||||
!!! note
|
!!! note
|
||||||
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
|
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
|
||||||
|
|
||||||
|
## Webhooks Configuration
|
||||||
|
|
||||||
|
If you have opted to enable the webhooks, set `WEBHOOKS_ENABLED = True` and define the relevant `REDIS` database parameters. Below is an example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
WEBHOOKS_ENABLED = True
|
||||||
|
REDIS = {
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': 6379,
|
||||||
|
'PASSWORD': '',
|
||||||
|
'DATABASE': 0,
|
||||||
|
'DEFAULT_TIMEOUT': 300,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Run Database Migrations
|
# Run Database Migrations
|
||||||
|
|
||||||
!!! warning
|
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):
|
||||||
The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
|
|
||||||
|
|
||||||
Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cd /opt/netbox/netbox/
|
# cd /opt/netbox/netbox/
|
||||||
# python manage.py migrate
|
# python3 manage.py migrate
|
||||||
Operations to perform:
|
Operations to perform:
|
||||||
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
||||||
Running migrations:
|
Running migrations:
|
||||||
@@ -182,7 +201,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:
|
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
|
```no-highlight
|
||||||
# python manage.py createsuperuser
|
# python3 manage.py createsuperuser
|
||||||
Username: admin
|
Username: admin
|
||||||
Email address: admin@example.com
|
Email address: admin@example.com
|
||||||
Password:
|
Password:
|
||||||
@@ -193,7 +212,7 @@ Superuser created successfully.
|
|||||||
# Collect Static Files
|
# Collect Static Files
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# python manage.py collectstatic --no-input
|
# python3 manage.py collectstatic --no-input
|
||||||
|
|
||||||
You have requested to collect static files at the destination
|
You have requested to collect static files at the destination
|
||||||
location as specified in your settings:
|
location as specified in your settings:
|
||||||
@@ -214,7 +233,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.
|
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
|
```no-highlight
|
||||||
# python manage.py loaddata initial_data
|
# python3 manage.py loaddata initial_data
|
||||||
Installed 43 object(s) from 4 fixture(s)
|
Installed 43 object(s) from 4 fixture(s)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -223,7 +242,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:
|
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# python manage.py runserver 0.0.0.0:8000 --insecure
|
# python3 manage.py runserver 0.0.0.0:8000 --insecure
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
|
|
||||||
System check identified no issues (0 silenced).
|
System check identified no issues (0 silenced).
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
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
|
!!! note
|
||||||
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
The installation instructions provided here have been tested to work on Ubuntu 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
|
# Installation
|
||||||
|
|
||||||
**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
|
```no-highlight
|
||||||
# apt-get update
|
# apt-get update
|
||||||
# apt-get install -y postgresql libpq-dev
|
# apt-get install -y postgresql libpq-dev
|
||||||
@@ -14,22 +19,26 @@ NetBox requires a PostgreSQL database to store data. (Please note that MySQL is
|
|||||||
|
|
||||||
**CentOS**
|
**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
|
```no-highlight
|
||||||
# yum install -y postgresql postgresql-server postgresql-devel
|
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
|
||||||
# postgresql-setup initdb
|
# 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
|
```no-highlight
|
||||||
host all all 127.0.0.1/32 md5
|
host all all 127.0.0.1/32 md5
|
||||||
host all all ::1/128 md5
|
host all all ::1/128 md5
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, start the service:
|
Then, start the service and enable it to run at boot:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# systemctl start postgresql
|
# systemctl start postgresql-9.6
|
||||||
|
# systemctl enable postgresql-9.6
|
||||||
```
|
```
|
||||||
|
|
||||||
# Database Creation
|
# Database Creation
|
||||||
@@ -41,7 +50,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# sudo -u postgres psql
|
# sudo -u postgres psql
|
||||||
psql (9.3.13)
|
psql (9.4.5)
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
postgres=# CREATE DATABASE netbox;
|
postgres=# CREATE DATABASE netbox;
|
||||||
@@ -53,10 +62,10 @@ GRANT
|
|||||||
postgres=# \q
|
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
|
```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.
|
||||||
|
|||||||
@@ -12,25 +12,37 @@ Download and extract the latest version:
|
|||||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -sf netbox-X.Y.Z/ netbox
|
# ln -sfn netbox-X.Y.Z/ netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the 'configuration.py' you created when first installing to the new version:
|
Copy the 'configuration.py' you created when first installing to the new version:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
|
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/
|
||||||
|
```
|
||||||
|
|
||||||
|
Also make sure to copy over any reports that you've made. Note that if you made them in a separate directory (`/opt/netbox-reports` for example), then you will not need to copy them - the config file that you copied earlier will point to the correct location.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/
|
||||||
```
|
```
|
||||||
|
|
||||||
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
# cp netbox-X.Y.Z/gunicorn_config.py netbox/gunicorn_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the LDAP configuration if using LDAP:
|
Copy the LDAP configuration if using LDAP:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option B: Clone the Git Repository (latest master release)
|
## Option B: Clone the Git Repository (latest master release)
|
||||||
@@ -53,7 +65,7 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
!!! 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.
|
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. Note that Python 2 will no longer be supported in NetBox v2.5.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# ./upgrade.sh -2
|
# ./upgrade.sh -2
|
||||||
@@ -80,3 +92,9 @@ Finally, restart the WSGI service to run the new code. If you followed this guid
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
# sudo supervisorctl restart netbox
|
# sudo supervisorctl restart netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If using webhooks, also restart the Redis worker:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# sudo supervisorctl restart netbox-rqworker
|
||||||
|
```
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
|
|||||||
ProxyPass !
|
ProxyPass !
|
||||||
</Location>
|
</Location>
|
||||||
|
|
||||||
|
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
|
||||||
ProxyPass / http://127.0.0.1:8001/
|
ProxyPass / http://127.0.0.1:8001/
|
||||||
ProxyPassReverse / http://127.0.0.1:8001/
|
ProxyPassReverse / http://127.0.0.1:8001/
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
@@ -92,6 +93,7 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
# a2enmod proxy
|
# a2enmod proxy
|
||||||
# a2enmod proxy_http
|
# a2enmod proxy_http
|
||||||
|
# a2enmod headers
|
||||||
# a2ensite netbox
|
# a2ensite netbox
|
||||||
# service apache2 restart
|
# service apache2 restart
|
||||||
```
|
```
|
||||||
@@ -100,7 +102,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
|
|||||||
|
|
||||||
# gunicorn Installation
|
# gunicorn Installation
|
||||||
|
|
||||||
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
|
Install gunicorn:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# pip3 install gunicorn
|
# pip3 install gunicorn
|
||||||
@@ -131,6 +133,11 @@ Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command`
|
|||||||
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||||
directory = /opt/netbox/netbox/
|
directory = /opt/netbox/netbox/
|
||||||
user = www-data
|
user = www-data
|
||||||
|
|
||||||
|
[program:netbox-rqworker]
|
||||||
|
command = python3 /opt/netbox/netbox/manage.py rqworker
|
||||||
|
directory = /opt/netbox/netbox/
|
||||||
|
user = www-data
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, restart the supervisor service to detect and run the gunicorn service:
|
Then, restart the supervisor service to detect and run the gunicorn service:
|
||||||
|
|||||||
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, DEVICE_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=DEVICE_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=DEVICE_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.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
NetBox includes a Python shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
./manage.py nbshell
|
./manage.py nbshell
|
||||||
@@ -86,7 +86,7 @@ The `count()` method can be appended to the queryset to return a count of object
|
|||||||
982
|
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."
|
Relationships with other models can be traversed by concatenating 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')
|
>>> Device.objects.filter(tenant__name='Pied Piper')
|
||||||
@@ -18,14 +18,18 @@ pages:
|
|||||||
- 'IPAM': 'data-model/ipam.md'
|
- 'IPAM': 'data-model/ipam.md'
|
||||||
- 'Secrets': 'data-model/secrets.md'
|
- 'Secrets': 'data-model/secrets.md'
|
||||||
- 'Tenancy': 'data-model/tenancy.md'
|
- 'Tenancy': 'data-model/tenancy.md'
|
||||||
|
- 'Virtualization': 'data-model/virtualization.md'
|
||||||
- 'Extras': 'data-model/extras.md'
|
- 'Extras': 'data-model/extras.md'
|
||||||
- 'API':
|
- 'API':
|
||||||
- 'Overview': 'api/overview.md'
|
- 'Overview': 'api/overview.md'
|
||||||
- 'Authentication': 'api/authentication.md'
|
- 'Authentication': 'api/authentication.md'
|
||||||
- 'Working with Secrets': 'api/working-with-secrets.md'
|
- 'Working with Secrets': 'api/working-with-secrets.md'
|
||||||
- 'Examples': 'api/examples.md'
|
- 'Examples': 'api/examples.md'
|
||||||
- 'Shell':
|
- 'Miscellaneous':
|
||||||
- 'Introduction': 'shell/intro.md'
|
- 'Reports': 'miscellaneous/reports.md'
|
||||||
|
- 'Shell': 'miscellaneous/shell.md'
|
||||||
|
- 'Development':
|
||||||
|
- 'Utility Views': 'development/utility-views.md'
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition:
|
- admonition:
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -14,16 +16,17 @@ from utilities.api import ValidatedModelSerializer
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ProviderSerializer(CustomFieldModelSerializer):
|
class ProviderSerializer(CustomFieldModelSerializer):
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||||
'custom_fields',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedProviderSerializer(serializers.ModelSerializer):
|
class NestedProviderSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -31,16 +34,6 @@ class NestedProviderSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit types
|
# Circuit types
|
||||||
#
|
#
|
||||||
@@ -52,7 +45,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
|
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -66,18 +59,20 @@ class NestedCircuitTypeSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class CircuitSerializer(CustomFieldModelSerializer):
|
class CircuitSerializer(CustomFieldModelSerializer):
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
|
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||||
type = NestedCircuitTypeSerializer()
|
type = NestedCircuitTypeSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||||
'custom_fields',
|
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedCircuitSerializer(serializers.ModelSerializer):
|
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -85,33 +80,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'cid']
|
fields = ['id', 'url', 'cid']
|
||||||
|
|
||||||
|
|
||||||
class WritableCircuitSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Circuit
|
|
||||||
fields = [
|
|
||||||
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
|
||||||
'custom_fields',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit Terminations
|
# Circuit Terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
class CircuitTerminationSerializer(ValidatedModelSerializer):
|
||||||
circuit = NestedCircuitSerializer()
|
circuit = NestedCircuitSerializer()
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
interface = InterfaceSerializer()
|
interface = InterfaceSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CircuitTermination
|
|
||||||
fields = [
|
|
||||||
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class CircuitsRootView(routers.APIRootView):
|
|||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.APIRootView = CircuitsRootView
|
router.APIRootView = CircuitsRootView
|
||||||
|
|
||||||
|
# Field choices
|
||||||
|
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, base_name='field-choice')
|
||||||
|
|
||||||
# Providers
|
# Providers
|
||||||
router.register(r'providers', views.ProviderViewSet)
|
router.register(r'providers', views.ProviderViewSet)
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from utilities.api import WritableSerializerMixin
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
|
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Field choices
|
||||||
|
#
|
||||||
|
|
||||||
|
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||||
|
fields = (
|
||||||
|
(Circuit, ['status']),
|
||||||
|
(CircuitTermination, ['term_side']),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class ProviderViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Provider.objects.all()
|
queryset = Provider.objects.all()
|
||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
write_serializer_class = serializers.WritableProviderSerializer
|
|
||||||
filter_class = filters.ProviderFilter
|
filter_class = filters.ProviderFilter
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def graphs(self, request, pk=None):
|
def graphs(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
A convenience method for rendering graphs for a particular provider.
|
A convenience method for rendering graphs for a particular provider.
|
||||||
@@ -50,10 +58,9 @@ class CircuitTypeViewSet(ModelViewSet):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class CircuitViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
write_serializer_class = serializers.WritableCircuitSerializer
|
|
||||||
filter_class = filters.CircuitFilter
|
filter_class = filters.CircuitFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -61,8 +68,7 @@ class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# Circuit Terminations
|
# Circuit Terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationViewSet(WritableSerializerMixin, ModelViewSet):
|
class CircuitTerminationViewSet(ModelViewSet):
|
||||||
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
||||||
serializer_class = serializers.CircuitTerminationSerializer
|
serializer_class = serializers.CircuitTerminationSerializer
|
||||||
write_serializer_class = serializers.WritableCircuitTerminationSerializer
|
|
||||||
filter_class = filters.CircuitTerminationFilter
|
filter_class = filters.CircuitTerminationFilter
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ class CircuitsConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import circuits.signals
|
import circuits.signals
|
||||||
|
|
||||||
|
# register webhook signals
|
||||||
|
from extras.webhooks import register_signals
|
||||||
|
from .models import Circuit, Provider
|
||||||
|
register_signals([Circuit, Provider])
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
from __future__ import unicode_literals
|
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
|
# CircuitTermination sides
|
||||||
TERM_SIDE_A = 'A'
|
TERM_SIDE_A = 'A'
|
||||||
TERM_SIDE_Z = 'Z'
|
TERM_SIDE_Z = 'Z'
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
from utilities.filters import NumericInFilter
|
||||||
|
from .constants import CIRCUIT_STATUS_CHOICES
|
||||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
@@ -78,12 +81,16 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Circuit type (slug)',
|
label='Circuit type (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=CIRCUIT_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = NullableModelMultipleChoiceFilter(
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant__slug',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
@@ -99,6 +106,9 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack
|
from dcim.models import Site, Device, Interface, Rack
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin,
|
||||||
SmallTextarea, SlugField,
|
ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
from .constants import CIRCUIT_STATUS_CHOICES
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
@@ -22,10 +23,11 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
|
|||||||
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
|
||||||
widgets = {
|
widgets = {
|
||||||
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
||||||
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
||||||
@@ -44,7 +46,7 @@ class ProviderCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
fields = Provider.csv_headers
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': 'Provider name',
|
'name': 'Provider name',
|
||||||
'asn': '32-bit autonomous system number',
|
'asn': '32-bit autonomous system number',
|
||||||
@@ -53,7 +55,7 @@ class ProviderCSVForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
asn = forms.IntegerField(required=False, label='ASN')
|
asn = forms.IntegerField(required=False, label='ASN')
|
||||||
account = forms.CharField(max_length=30, required=False, label='Account number')
|
account = forms.CharField(max_length=30, required=False, label='Account number')
|
||||||
@@ -85,18 +87,30 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
|||||||
fields = ['name', 'slug']
|
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
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||||
'comments',
|
'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
'cid': "Unique circuit ID",
|
||||||
@@ -122,6 +136,11 @@ class CircuitCSVForm(forms.ModelForm):
|
|||||||
'invalid_choice': 'Invalid circuit type.'
|
'invalid_choice': 'Invalid circuit type.'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=CIRCUIT_STATUS_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='Operational status'
|
||||||
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -134,13 +153,16 @@ class CircuitCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
fields = [
|
||||||
|
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||||
provider = forms.ModelChoiceField(queryset=Provider.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)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
@@ -161,10 +183,16 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||||
to_field_name='slug'
|
to_field_name='slug'
|
||||||
)
|
)
|
||||||
|
status = AnnotatedMultipleChoiceField(
|
||||||
|
choices=CIRCUIT_STATUS_CHOICES,
|
||||||
|
annotate=Circuit.objects.all(),
|
||||||
|
annotate_field='status',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||||
|
|||||||
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
netbox/circuits/migrations/0011_tags.py
Normal file
27
netbox/circuits/migrations/0011_tags.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('circuits', '0010_circuit_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='provider',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
||||||
45
netbox/circuits/migrations/0012_change_logging.py
Normal file
45
netbox/circuits/migrations/0012_change_logging.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-06-13 17:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0011_tags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuittype',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuittype',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,32 +4,64 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
from dcim.constants import STATUS_CLASSES
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, ObjectChange
|
||||||
from tenancy.models import Tenant
|
from utilities.models import ChangeLoggedModel
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import serialize_object
|
||||||
from utilities.models import CreatedUpdatedModel
|
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||||
from .constants import *
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
stores information pertinent to the user's relationship with the Provider.
|
stores information pertinent to the user's relationship with the Provider.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
unique=True
|
||||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
)
|
||||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
slug = models.SlugField(
|
||||||
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
unique=True
|
||||||
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
|
)
|
||||||
comments = models.TextField(blank=True)
|
asn = ASNField(
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='ASN'
|
||||||
|
)
|
||||||
|
account = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Account number'
|
||||||
|
)
|
||||||
|
portal_url = models.URLField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Portal'
|
||||||
|
)
|
||||||
|
noc_contact = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='NOC contact'
|
||||||
|
)
|
||||||
|
admin_contact = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Admin contact'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
serializer = 'circuits.api.serializers.ProviderSerializer'
|
||||||
|
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -41,23 +73,34 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return reverse('circuits:provider', args=[self.slug])
|
return reverse('circuits:provider', args=[self.slug])
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return (
|
||||||
self.name,
|
self.name,
|
||||||
self.slug,
|
self.slug,
|
||||||
self.asn,
|
self.asn,
|
||||||
self.account,
|
self.account,
|
||||||
self.portal_url,
|
self.portal_url,
|
||||||
])
|
self.noc_contact,
|
||||||
|
self.admin_contact,
|
||||||
|
self.comments,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CircuitType(models.Model):
|
class CircuitType(ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||||
"Long Haul," "Metro," or "Out-of-Band".
|
"Long Haul," "Metro," or "Out-of-Band".
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = 'circuits.api.serializers.CircuitTypeSerializer'
|
||||||
|
csv_headers = ['name', 'slug']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -68,25 +111,73 @@ class CircuitType(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.name,
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||||
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
|
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
|
||||||
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
|
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
|
||||||
"""
|
"""
|
||||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
cid = models.CharField(
|
||||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
max_length=50,
|
||||||
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
verbose_name='Circuit ID'
|
||||||
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')
|
provider = models.ForeignKey(
|
||||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
to='circuits.Provider',
|
||||||
description = models.CharField(max_length=100, blank=True)
|
on_delete=models.PROTECT,
|
||||||
comments = models.TextField(blank=True)
|
related_name='circuits'
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
)
|
||||||
|
type = models.ForeignKey(
|
||||||
|
to='CircuitType',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuits'
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=CIRCUIT_STATUS_CHOICES,
|
||||||
|
default=CIRCUIT_STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuits',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
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)')
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
serializer = 'circuits.api.serializers.CircuitSerializer'
|
||||||
|
csv_headers = [
|
||||||
|
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['provider', 'cid']
|
ordering = ['provider', 'cid']
|
||||||
@@ -99,15 +190,20 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return reverse('circuits:circuit', args=[self.pk])
|
return reverse('circuits:circuit', args=[self.pk])
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return (
|
||||||
self.cid,
|
self.cid,
|
||||||
self.provider.name,
|
self.provider.name,
|
||||||
self.type.name,
|
self.type.name,
|
||||||
|
self.get_status_display(),
|
||||||
self.tenant.name if self.tenant else None,
|
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.commit_rate,
|
||||||
self.description,
|
self.description,
|
||||||
])
|
self.comments,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_status_class(self):
|
||||||
|
return STATUS_CLASSES[self.status]
|
||||||
|
|
||||||
def _get_termination(self, side):
|
def _get_termination(self, side):
|
||||||
for ct in self.terminations.all():
|
for ct in self.terminations.all():
|
||||||
@@ -126,19 +222,47 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CircuitTermination(models.Model):
|
class CircuitTermination(models.Model):
|
||||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
circuit = models.ForeignKey(
|
||||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
to='circuits.Circuit',
|
||||||
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
on_delete=models.CASCADE,
|
||||||
interface = models.OneToOneField(
|
related_name='terminations'
|
||||||
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
|
)
|
||||||
|
term_side = models.CharField(
|
||||||
|
max_length=1,
|
||||||
|
choices=TERM_SIDE_CHOICES,
|
||||||
|
verbose_name='Termination'
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuit_terminations'
|
||||||
|
)
|
||||||
|
interface = models.OneToOneField(
|
||||||
|
to='dcim.Interface',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='circuit_termination',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
port_speed = models.PositiveIntegerField(
|
||||||
|
verbose_name='Port speed (Kbps)'
|
||||||
)
|
)
|
||||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
|
||||||
upstream_speed = models.PositiveIntegerField(
|
upstream_speed = models.PositiveIntegerField(
|
||||||
blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Upstream speed (Kbps)',
|
||||||
help_text='Upstream speed, if different from port speed'
|
help_text='Upstream speed, if different from port speed'
|
||||||
)
|
)
|
||||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
xconnect_id = models.CharField(
|
||||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
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)'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['circuit', 'term_side']
|
ordering = ['circuit', 'term_side']
|
||||||
@@ -147,6 +271,19 @@ class CircuitTermination(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||||
|
|
||||||
|
def log_change(self, user, request_id, action):
|
||||||
|
"""
|
||||||
|
Reference the parent circuit when recording the change.
|
||||||
|
"""
|
||||||
|
ObjectChange(
|
||||||
|
user=user,
|
||||||
|
request_id=request_id,
|
||||||
|
changed_object=self,
|
||||||
|
related_object=self.circuit,
|
||||||
|
action=action,
|
||||||
|
object_data=serialize_object(self)
|
||||||
|
).save()
|
||||||
|
|
||||||
def get_peer_termination(self):
|
def get_peer_termination(self):
|
||||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
CIRCUITTYPE_ACTIONS = """
|
CIRCUITTYPE_ACTIONS = """
|
||||||
|
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.circuit.change_circuittype %}
|
{% 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>
|
<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 %}
|
{% 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
|
# Providers
|
||||||
@@ -60,16 +83,11 @@ class CircuitTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
cid = tables.LinkColumn(verbose_name='ID')
|
cid = tables.LinkColumn(verbose_name='ID')
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
a_side = tables.LinkColumn(
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
'dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
||||||
args=[Accessor('termination_a.site.slug')]
|
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
||||||
)
|
|
||||||
z_side = tables.LinkColumn(
|
|
||||||
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
|
||||||
args=[Accessor('termination_z.site.slug')]
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
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')
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||||
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
from extras.models import Graph
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.testing import HttpStatusMixin
|
||||||
|
|
||||||
|
|
||||||
class ProviderTest(HttpStatusMixin, APITestCase):
|
class ProviderTest(HttpStatusMixin, APITestCase):
|
||||||
@@ -68,7 +69,7 @@ class ProviderTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:provider-list')
|
url = reverse('circuits-api:provider-list')
|
||||||
response = self.client.post(url, data, **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Provider.objects.count(), 4)
|
self.assertEqual(Provider.objects.count(), 4)
|
||||||
@@ -76,6 +77,32 @@ class ProviderTest(HttpStatusMixin, APITestCase):
|
|||||||
self.assertEqual(provider4.name, data['name'])
|
self.assertEqual(provider4.name, data['name'])
|
||||||
self.assertEqual(provider4.slug, data['slug'])
|
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):
|
def test_update_provider(self):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -84,7 +111,7 @@ class ProviderTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
|
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
|
||||||
response = self.client.put(url, data, **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(Provider.objects.count(), 3)
|
self.assertEqual(Provider.objects.count(), 3)
|
||||||
@@ -135,7 +162,7 @@ class CircuitTypeTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuittype-list')
|
url = reverse('circuits-api:circuittype-list')
|
||||||
response = self.client.post(url, data, **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(CircuitType.objects.count(), 4)
|
self.assertEqual(CircuitType.objects.count(), 4)
|
||||||
@@ -151,7 +178,7 @@ class CircuitTypeTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
|
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
|
||||||
response = self.client.put(url, data, **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(CircuitType.objects.count(), 3)
|
self.assertEqual(CircuitType.objects.count(), 3)
|
||||||
@@ -204,10 +231,11 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
|||||||
'cid': 'TEST0004',
|
'cid': 'TEST0004',
|
||||||
'provider': self.provider1.pk,
|
'provider': self.provider1.pk,
|
||||||
'type': self.circuittype1.pk,
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuit-list')
|
url = reverse('circuits-api:circuit-list')
|
||||||
response = self.client.post(url, data, **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Circuit.objects.count(), 4)
|
self.assertEqual(Circuit.objects.count(), 4)
|
||||||
@@ -216,6 +244,38 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
|||||||
self.assertEqual(circuit4.provider_id, data['provider'])
|
self.assertEqual(circuit4.provider_id, data['provider'])
|
||||||
self.assertEqual(circuit4.type_id, data['type'])
|
self.assertEqual(circuit4.type_id, data['type'])
|
||||||
|
|
||||||
|
def test_create_circuit_bulk(self):
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'cid': 'TEST0004',
|
||||||
|
'provider': self.provider1.pk,
|
||||||
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'cid': 'TEST0005',
|
||||||
|
'provider': self.provider1.pk,
|
||||||
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'cid': 'TEST0006',
|
||||||
|
'provider': self.provider1.pk,
|
||||||
|
'type': self.circuittype1.pk,
|
||||||
|
'status': CIRCUIT_STATUS_ACTIVE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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):
|
def test_update_circuit(self):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -225,7 +285,7 @@ class CircuitTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
|
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
|
||||||
response = self.client.put(url, data, **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(Circuit.objects.count(), 3)
|
self.assertEqual(Circuit.objects.count(), 3)
|
||||||
@@ -292,7 +352,7 @@ class CircuitTerminationTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuittermination-list')
|
url = reverse('circuits-api:circuittermination-list')
|
||||||
response = self.client.post(url, data, **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(CircuitTermination.objects.count(), 4)
|
self.assertEqual(CircuitTermination.objects.count(), 4)
|
||||||
@@ -312,7 +372,7 @@ class CircuitTerminationTest(HttpStatusMixin, APITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
||||||
response = self.client.put(url, data, **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(CircuitTermination.objects.count(), 3)
|
self.assertEqual(CircuitTermination.objects.count(), 3)
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from extras.views import ObjectChangeLogView
|
||||||
from . import views
|
from . import views
|
||||||
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
app_name = 'circuits'
|
app_name = 'circuits'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -17,12 +18,15 @@ urlpatterns = [
|
|||||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), 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-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||||
|
url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||||
|
|
||||||
# Circuit types
|
# Circuit types
|
||||||
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||||
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.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/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||||
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||||
|
url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||||
|
|
||||||
# Circuits
|
# Circuits
|
||||||
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
||||||
@@ -33,6 +37,7 @@ urlpatterns = [
|
|||||||
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), 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+)/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+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||||
|
url(r'^circuits/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||||
|
|
||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
@@ -15,7 +14,8 @@ from utilities.views import (
|
|||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from . import filters, forms, tables
|
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
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -52,7 +52,7 @@ class ProviderView(View):
|
|||||||
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
|
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.add_provider'
|
permission_required = 'circuits.add_provider'
|
||||||
model = Provider
|
model = Provider
|
||||||
form_class = forms.ProviderForm
|
model_form = forms.ProviderForm
|
||||||
template_name = 'circuits/provider_edit.html'
|
template_name = 'circuits/provider_edit.html'
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
@@ -104,16 +104,21 @@ class CircuitTypeListView(ObjectListView):
|
|||||||
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.add_circuittype'
|
permission_required = 'circuits.add_circuittype'
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
form_class = forms.CircuitTypeForm
|
model_form = forms.CircuitTypeForm
|
||||||
|
default_return_url = 'circuits:circuittype_list'
|
||||||
def get_return_url(self, request, obj):
|
|
||||||
return reverse('circuits:circuittype_list')
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeEditView(CircuitTypeCreateView):
|
class CircuitTypeEditView(CircuitTypeCreateView):
|
||||||
permission_required = 'circuits.change_circuittype'
|
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):
|
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittype'
|
permission_required = 'circuits.delete_circuittype'
|
||||||
cls = CircuitType
|
cls = CircuitType
|
||||||
@@ -127,7 +132,11 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitListView(ObjectListView):
|
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 = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@@ -160,7 +169,7 @@ class CircuitView(View):
|
|||||||
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.add_circuit'
|
permission_required = 'circuits.add_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
form_class = forms.CircuitForm
|
model_form = forms.CircuitForm
|
||||||
template_name = 'circuits/circuit_edit.html'
|
template_name = 'circuits/circuit_edit.html'
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
@@ -253,7 +262,7 @@ def circuit_terminations_swap(request, pk):
|
|||||||
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.add_circuittermination'
|
permission_required = 'circuits.add_circuittermination'
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
form_class = forms.CircuitTerminationForm
|
model_form = forms.CircuitTerminationForm
|
||||||
template_name = 'circuits/circuittermination_edit.html'
|
template_name = 'circuits/circuittermination_edit.html'
|
||||||
|
|
||||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
|
|||||||
@@ -1,28 +1,36 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
|
from dcim.constants import (
|
||||||
|
CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES,
|
||||||
|
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||||
|
)
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||||
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
|
||||||
)
|
)
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
|
from ipam.models import IPAddress, VLAN
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
|
from utilities.api import (
|
||||||
|
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
|
)
|
||||||
|
from virtualization.models import Cluster
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedRegionSerializer(serializers.ModelSerializer):
|
class NestedRegionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -31,14 +39,7 @@ class NestedRegionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RegionSerializer(serializers.ModelSerializer):
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
parent = NestedRegionSerializer()
|
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Region
|
|
||||||
fields = ['id', 'name', 'slug', 'parent']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableRegionSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@@ -50,19 +51,23 @@ class WritableRegionSerializer(ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteSerializer(CustomFieldModelSerializer):
|
class SiteSerializer(CustomFieldModelSerializer):
|
||||||
region = NestedRegionSerializer()
|
status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
|
||||||
tenant = NestedTenantSerializer()
|
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
time_zone = TimeZoneField(required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
|
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes',
|
||||||
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
|
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedSiteSerializer(serializers.ModelSerializer):
|
class NestedSiteSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -70,21 +75,11 @@ class NestedSiteSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableSiteSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Site
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
|
||||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackGroupSerializer(serializers.ModelSerializer):
|
class RackGroupSerializer(ValidatedModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -92,7 +87,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'site']
|
fields = ['id', 'name', 'slug', 'site']
|
||||||
|
|
||||||
|
|
||||||
class NestedRackGroupSerializer(serializers.ModelSerializer):
|
class NestedRackGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -100,13 +95,6 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackGroupSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RackGroup
|
|
||||||
fields = ['id', 'name', 'slug', 'site']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
@@ -118,7 +106,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class NestedRackRoleSerializer(serializers.ModelSerializer):
|
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -132,21 +120,40 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class RackSerializer(CustomFieldModelSerializer):
|
class RackSerializer(CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
group = NestedRackGroupSerializer()
|
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
role = NestedRackRoleSerializer()
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
|
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
|
||||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height',
|
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||||
'desc_units', 'comments', 'custom_fields',
|
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
|
||||||
|
# prevents facility_id from being interpreted as a required field.
|
||||||
|
validators = [
|
||||||
|
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
class NestedRackSerializer(serializers.ModelSerializer):
|
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
|
||||||
|
if data.get('facility_id', None):
|
||||||
|
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
|
||||||
|
validator.set_context(self)
|
||||||
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(RackSerializer, self).validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class NestedRackSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -154,39 +161,11 @@ class NestedRackSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'display_name']
|
fields = ['id', 'url', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Rack
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
|
||||||
'comments', 'custom_fields',
|
|
||||||
]
|
|
||||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
|
|
||||||
# prevents facility_id from being interpreted as a required field.
|
|
||||||
validators = [
|
|
||||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'name'))
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of (site, facility_id) since we omitted the automatically-created validator from Meta.
|
|
||||||
if data.get('facility_id', None):
|
|
||||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'facility_id'))
|
|
||||||
validator.set_context(self)
|
|
||||||
validator(data)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super(WritableRackSerializer, self).validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack units
|
# Rack units
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedDeviceSerializer(serializers.ModelSerializer):
|
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -208,19 +187,14 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationSerializer(serializers.ModelSerializer):
|
class RackReservationSerializer(ValidatedModelSerializer):
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer()
|
||||||
|
user = NestedUserSerializer()
|
||||||
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackReservationSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RackReservation
|
|
||||||
fields = ['id', 'rack', 'units', 'description']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -234,7 +208,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class NestedManufacturerSerializer(serializers.ModelSerializer):
|
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -248,43 +222,34 @@ class NestedManufacturerSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class DeviceTypeSerializer(CustomFieldModelSerializer):
|
class DeviceTypeSerializer(CustomFieldModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES)
|
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||||
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES)
|
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
|
||||||
'instance_count',
|
'created', 'last_updated', 'instance_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceTypeSerializer(serializers.ModelSerializer):
|
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceType
|
|
||||||
fields = [
|
|
||||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console port templates
|
# Console port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -292,18 +257,11 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsolePortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console server port templates
|
# Console server port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -311,18 +269,11 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsoleServerPortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power port templates
|
# Power port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -330,18 +281,11 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerPortTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power outlet templates
|
# Power outlet templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -349,27 +293,13 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerOutletTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface templates
|
# Interface templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceTemplateSerializer(serializers.ModelSerializer):
|
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = InterfaceTemplate
|
|
||||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
@@ -380,7 +310,7 @@ class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
|||||||
# Device bay templates
|
# Device bay templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -388,13 +318,6 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceBayTemplate
|
|
||||||
fields = ['id', 'device_type', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
@@ -403,10 +326,10 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['id', 'name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -419,13 +342,14 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PlatformSerializer(ValidatedModelSerializer):
|
class PlatformSerializer(ValidatedModelSerializer):
|
||||||
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
|
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client']
|
||||||
|
|
||||||
|
|
||||||
class NestedPlatformSerializer(serializers.ModelSerializer):
|
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -446,46 +370,49 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'family', 'address']
|
fields = ['id', 'url', 'family', 'address']
|
||||||
|
|
||||||
|
|
||||||
|
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||||
|
class NestedClusterSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Cluster
|
||||||
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||||
|
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
|
master = NestedDeviceSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'url', 'master']
|
||||||
|
|
||||||
|
|
||||||
class DeviceSerializer(CustomFieldModelSerializer):
|
class DeviceSerializer(CustomFieldModelSerializer):
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
device_role = NestedDeviceRoleSerializer()
|
device_role = NestedDeviceRoleSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
platform = NestedPlatformSerializer()
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||||
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
|
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
|
||||||
status = ChoiceFieldSerializer(choices=STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||||
primary_ip = DeviceIPAddressSerializer()
|
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||||
primary_ip4 = DeviceIPAddressSerializer()
|
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
primary_ip6 = DeviceIPAddressSerializer()
|
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
parent_device = serializers.SerializerMethodField()
|
parent_device = serializers.SerializerMethodField()
|
||||||
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
|
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
'comments', 'custom_fields',
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||||
]
|
'last_updated',
|
||||||
|
|
||||||
def get_parent_device(self, obj):
|
|
||||||
try:
|
|
||||||
device_bay = obj.parent_bay
|
|
||||||
except DeviceBay.DoesNotExist:
|
|
||||||
return None
|
|
||||||
context = {'request': self.context['request']}
|
|
||||||
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
|
||||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Device
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
|
|
||||||
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
|
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@@ -498,101 +425,121 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
|
|||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
# Enforce model validation
|
# Enforce model validation
|
||||||
super(WritableDeviceSerializer, self).validate(data)
|
super(DeviceSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_parent_device(self, obj):
|
||||||
|
try:
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
except DeviceBay.DoesNotExist:
|
||||||
|
return None
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||||
|
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
|
config_context = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta(DeviceSerializer.Meta):
|
||||||
|
fields = [
|
||||||
|
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
|
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
|
||||||
|
'config_context', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_config_context(self, obj):
|
||||||
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
class ConsoleServerPortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['id', 'device', 'name', 'connected_console']
|
fields = ['id', 'device', 'name', 'connected_console', 'tags']
|
||||||
read_only_fields = ['connected_console']
|
read_only_fields = ['connected_console']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
|
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['id', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortSerializer(serializers.ModelSerializer):
|
class ConsolePortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
cs_port = ConsoleServerPortSerializer()
|
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
fields = ['id', 'device', 'name', 'cs_port', 'connection_status', 'tags']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ConsolePort
|
|
||||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletSerializer(serializers.ModelSerializer):
|
class PowerOutletSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['id', 'device', 'name', 'connected_port']
|
fields = ['id', 'device', 'name', 'connected_port', 'tags']
|
||||||
read_only_fields = ['connected_port']
|
read_only_fields = ['connected_port']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletSerializer(ValidatedModelSerializer):
|
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['id', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortSerializer(serializers.ModelSerializer):
|
class PowerPortSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
power_outlet = PowerOutletSerializer()
|
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status', 'tags']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PowerPort
|
|
||||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedInterfaceSerializer(serializers.ModelSerializer):
|
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'device', 'name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||||
@@ -603,8 +550,8 @@ class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'cid']
|
fields = ['id', 'url', 'cid']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
|
class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
|
||||||
circuit = InterfaceNestedCircuitSerializer()
|
circuit = InterfaceNestedCircuitSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
@@ -613,21 +560,59 @@ class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(serializers.ModelSerializer):
|
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
|
||||||
|
class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLAN
|
||||||
|
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
|
||||||
lag = NestedInterfaceSerializer()
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||||
circuit_termination = InterfaceCircuitTerminationSerializer()
|
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||||
|
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
|
||||||
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
serializer=InterfaceVLANSerializer,
|
||||||
|
required=False,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||||
'is_connected', 'interface_connection', 'circuit_termination',
|
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# All associated VLANs be global or assigned to the parent device's site.
|
||||||
|
device = self.instance.device if self.instance else data.get('device')
|
||||||
|
untagged_vlan = data.get('untagged_vlan')
|
||||||
|
if untagged_vlan and untagged_vlan.site not in [device.site, None]:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
|
||||||
|
"global.".format(untagged_vlan)
|
||||||
|
})
|
||||||
|
for vlan in data.get('tagged_vlans', []):
|
||||||
|
if vlan.site not in [device.site, None]:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
|
||||||
|
"be global.".format(vlan)
|
||||||
|
})
|
||||||
|
|
||||||
|
return super(InterfaceSerializer, self).validate(data)
|
||||||
|
|
||||||
def get_is_connected(self, obj):
|
def get_is_connected(self, obj):
|
||||||
"""
|
"""
|
||||||
Return True if the interface has a connected interface or circuit termination.
|
Return True if the interface has a connected interface or circuit termination.
|
||||||
@@ -643,50 +628,29 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_interface_connection(self, obj):
|
def get_interface_connection(self, obj):
|
||||||
if obj.connection:
|
if obj.connection:
|
||||||
return OrderedDict((
|
context = {
|
||||||
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
|
'request': self.context['request'],
|
||||||
('status', obj.connection.connection_status),
|
'interface': obj.connected_interface,
|
||||||
))
|
}
|
||||||
|
return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class PeerInterfaceSerializer(serializers.ModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
|
||||||
device = NestedDeviceSerializer()
|
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
|
||||||
lag = NestedInterfaceSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Interface
|
|
||||||
fields = [
|
|
||||||
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
|
||||||
'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Interface
|
|
||||||
fields = [
|
|
||||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBaySerializer(serializers.ModelSerializer):
|
class DeviceBaySerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
installed_device = NestedDeviceSerializer()
|
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['id', 'device', 'name', 'installed_device']
|
fields = ['id', 'device', 'name', 'installed_device', 'tags']
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -694,36 +658,22 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBaySerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = DeviceBay
|
|
||||||
fields = ['id', 'device', 'name', 'installed_device']
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemSerializer(serializers.ModelSerializer):
|
class InventoryItemSerializer(ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
'description',
|
'description', 'tags',
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = InventoryItem
|
|
||||||
fields = [
|
|
||||||
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
|
||||||
'description',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -731,17 +681,17 @@ class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
|||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceConnectionSerializer(serializers.ModelSerializer):
|
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||||
interface_a = PeerInterfaceSerializer()
|
interface_a = NestedInterfaceSerializer()
|
||||||
interface_b = PeerInterfaceSerializer()
|
interface_b = NestedInterfaceSerializer()
|
||||||
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES)
|
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -749,8 +699,37 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'connection_status']
|
fields = ['id', 'url', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
|
||||||
|
"""
|
||||||
|
interface = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
fields = ['id', 'interface', 'connection_status']
|
||||||
|
|
||||||
|
def get_interface(self, obj):
|
||||||
|
return NestedInterfaceSerializer(self.context['interface'], context=self.context).data
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisSerializer(ValidatedModelSerializer):
|
||||||
|
master = NestedDeviceSerializer()
|
||||||
|
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'master', 'domain', 'tags']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'url']
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class DCIMRootView(routers.APIRootView):
|
|||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.APIRootView = DCIMRootView
|
router.APIRootView = DCIMRootView
|
||||||
|
|
||||||
|
# Field choices
|
||||||
|
router.register(r'_choices', views.DCIMFieldChoicesViewSet, base_name='field-choice')
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
router.register(r'regions', views.RegionViewSet)
|
router.register(r'regions', views.RegionViewSet)
|
||||||
router.register(r'sites', views.SiteViewSet)
|
router.register(r'sites', views.SiteViewSet)
|
||||||
@@ -57,6 +60,9 @@ router.register(r'console-connections', views.ConsoleConnectionViewSet, base_nam
|
|||||||
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
||||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||||
|
|
||||||
|
# Virtual chassis
|
||||||
|
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,57 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
from collections import OrderedDict
|
||||||
from rest_framework.mixins import ListModelMixin
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.openapi import Parameter
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.mixins import ListModelMixin
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||||
|
|
||||||
|
from dcim import filters
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
|
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable
|
||||||
from .exceptions import MissingFilterException
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from .exceptions import MissingFilterException
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Field choices
|
||||||
|
#
|
||||||
|
|
||||||
|
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||||
|
fields = (
|
||||||
|
(Device, ['face', 'status']),
|
||||||
|
(ConsolePort, ['connection_status']),
|
||||||
|
(Interface, ['form_factor', 'mode']),
|
||||||
|
(InterfaceConnection, ['connection_status']),
|
||||||
|
(InterfaceTemplate, ['form_factor']),
|
||||||
|
(PowerPort, ['connection_status']),
|
||||||
|
(Rack, ['type', 'width']),
|
||||||
|
(Site, ['status']),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class RegionViewSet(WritableSerializerMixin, ModelViewSet):
|
class RegionViewSet(ModelViewSet):
|
||||||
queryset = Region.objects.all()
|
queryset = Region.objects.all()
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
write_serializer_class = serializers.WritableRegionSerializer
|
|
||||||
filter_class = filters.RegionFilter
|
filter_class = filters.RegionFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -40,13 +59,12 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class SiteViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Site.objects.select_related('region', 'tenant')
|
queryset = Site.objects.select_related('region', 'tenant')
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
write_serializer_class = serializers.WritableSiteSerializer
|
|
||||||
filter_class = filters.SiteFilter
|
filter_class = filters.SiteFilter
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def graphs(self, request, pk=None):
|
def graphs(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
A convenience method for rendering graphs for a particular site.
|
A convenience method for rendering graphs for a particular site.
|
||||||
@@ -61,10 +79,9 @@ class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
|
class RackGroupViewSet(ModelViewSet):
|
||||||
queryset = RackGroup.objects.select_related('site')
|
queryset = RackGroup.objects.select_related('site')
|
||||||
serializer_class = serializers.RackGroupSerializer
|
serializer_class = serializers.RackGroupSerializer
|
||||||
write_serializer_class = serializers.WritableRackGroupSerializer
|
|
||||||
filter_class = filters.RackGroupFilter
|
filter_class = filters.RackGroupFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -82,13 +99,12 @@ class RackRoleViewSet(ModelViewSet):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class RackViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
write_serializer_class = serializers.WritableRackSerializer
|
|
||||||
filter_class = filters.RackFilter
|
filter_class = filters.RackFilter
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def units(self, request, pk=None):
|
def units(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
List rack units (by rack)
|
List rack units (by rack)
|
||||||
@@ -113,10 +129,9 @@ class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
|
class RackReservationViewSet(ModelViewSet):
|
||||||
queryset = RackReservation.objects.select_related('rack')
|
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
|
||||||
serializer_class = serializers.RackReservationSerializer
|
serializer_class = serializers.RackReservationSerializer
|
||||||
write_serializer_class = serializers.WritableRackReservationSerializer
|
|
||||||
filter_class = filters.RackReservationFilter
|
filter_class = filters.RackReservationFilter
|
||||||
|
|
||||||
# Assign user from request
|
# Assign user from request
|
||||||
@@ -138,10 +153,9 @@ class ManufacturerViewSet(ModelViewSet):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceType.objects.select_related('manufacturer')
|
queryset = DeviceType.objects.select_related('manufacturer')
|
||||||
serializer_class = serializers.DeviceTypeSerializer
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
write_serializer_class = serializers.WritableDeviceTypeSerializer
|
|
||||||
filter_class = filters.DeviceTypeFilter
|
filter_class = filters.DeviceTypeFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -149,45 +163,39 @@ class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||||
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
|
|
||||||
filter_class = filters.ConsolePortTemplateFilter
|
filter_class = filters.ConsolePortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||||
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
|
|
||||||
filter_class = filters.ConsoleServerPortTemplateFilter
|
filter_class = filters.ConsoleServerPortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class PowerPortTemplateViewSet(ModelViewSet):
|
||||||
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerPortTemplateSerializer
|
serializer_class = serializers.PowerPortTemplateSerializer
|
||||||
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
|
|
||||||
filter_class = filters.PowerPortTemplateFilter
|
filter_class = filters.PowerPortTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||||
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||||
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
|
|
||||||
filter_class = filters.PowerOutletTemplateFilter
|
filter_class = filters.PowerOutletTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class InterfaceTemplateViewSet(ModelViewSet):
|
||||||
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.InterfaceTemplateSerializer
|
serializer_class = serializers.InterfaceTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
|
|
||||||
filter_class = filters.InterfaceTemplateFilter
|
filter_class = filters.InterfaceTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||||
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||||
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
|
|
||||||
filter_class = filters.DeviceBayTemplateFilter
|
filter_class = filters.DeviceBayTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -215,17 +223,24 @@ class PlatformViewSet(ModelViewSet):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
class DeviceViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Device.objects.select_related(
|
queryset = Device.objects.select_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||||
|
'virtual_chassis__master',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.DeviceSerializer
|
|
||||||
write_serializer_class = serializers.WritableDeviceSerializer
|
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
|
|
||||||
@detail_route(url_path='napalm')
|
def get_serializer_class(self):
|
||||||
|
"""
|
||||||
|
Include rendered config context when retrieving a single Device.
|
||||||
|
"""
|
||||||
|
if self.action == 'retrieve':
|
||||||
|
return serializers.DeviceWithConfigContextSerializer
|
||||||
|
return serializers.DeviceSerializer
|
||||||
|
|
||||||
|
@action(detail=True, url_path='napalm')
|
||||||
def napalm(self, request, pk):
|
def napalm(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Execute a NAPALM method on a Device
|
Execute a NAPALM method on a Device
|
||||||
@@ -240,12 +255,14 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
device.platform
|
device.platform
|
||||||
))
|
))
|
||||||
|
|
||||||
# Check that NAPALM is installed and verify the configured driver
|
# Check that NAPALM is installed
|
||||||
try:
|
try:
|
||||||
import napalm
|
import napalm
|
||||||
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
|
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
|
||||||
|
|
||||||
|
# Validate the configured driver
|
||||||
try:
|
try:
|
||||||
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||||
except ModuleImportError:
|
except ModuleImportError:
|
||||||
@@ -269,12 +286,15 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# TODO: Improve error handling
|
# TODO: Improve error handling
|
||||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||||
ip_address = str(device.primary_ip.address.ip)
|
ip_address = str(device.primary_ip.address.ip)
|
||||||
|
optional_args = settings.NAPALM_ARGS.copy()
|
||||||
|
if device.platform.napalm_args is not None:
|
||||||
|
optional_args.update(device.platform.napalm_args)
|
||||||
d = driver(
|
d = driver(
|
||||||
hostname=ip_address,
|
hostname=ip_address,
|
||||||
username=settings.NAPALM_USERNAME,
|
username=settings.NAPALM_USERNAME,
|
||||||
password=settings.NAPALM_PASSWORD,
|
password=settings.NAPALM_PASSWORD,
|
||||||
timeout=settings.NAPALM_TIMEOUT,
|
timeout=settings.NAPALM_TIMEOUT,
|
||||||
optional_args=settings.NAPALM_ARGS
|
optional_args=optional_args
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
d.open()
|
d.open()
|
||||||
@@ -291,41 +311,36 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortViewSet(WritableSerializerMixin, ModelViewSet):
|
class ConsolePortViewSet(ModelViewSet):
|
||||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
write_serializer_class = serializers.WritableConsolePortSerializer
|
|
||||||
filter_class = filters.ConsolePortFilter
|
filter_class = filters.ConsolePortFilter
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortViewSet(WritableSerializerMixin, ModelViewSet):
|
class ConsoleServerPortViewSet(ModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
write_serializer_class = serializers.WritableConsoleServerPortSerializer
|
|
||||||
filter_class = filters.ConsoleServerPortFilter
|
filter_class = filters.ConsoleServerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(WritableSerializerMixin, ModelViewSet):
|
class PowerPortViewSet(ModelViewSet):
|
||||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
write_serializer_class = serializers.WritablePowerPortSerializer
|
|
||||||
filter_class = filters.PowerPortFilter
|
filter_class = filters.PowerPortFilter
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(WritableSerializerMixin, ModelViewSet):
|
class PowerOutletViewSet(ModelViewSet):
|
||||||
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
write_serializer_class = serializers.WritablePowerOutletSerializer
|
|
||||||
filter_class = filters.PowerOutletFilter
|
filter_class = filters.PowerOutletFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(WritableSerializerMixin, ModelViewSet):
|
class InterfaceViewSet(ModelViewSet):
|
||||||
queryset = Interface.objects.select_related('device')
|
queryset = Interface.objects.select_related('device')
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceSerializer
|
|
||||||
filter_class = filters.InterfaceFilter
|
filter_class = filters.InterfaceFilter
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def graphs(self, request, pk=None):
|
def graphs(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
A convenience method for rendering graphs for a particular interface.
|
A convenience method for rendering graphs for a particular interface.
|
||||||
@@ -336,17 +351,15 @@ class InterfaceViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayViewSet(WritableSerializerMixin, ModelViewSet):
|
class DeviceBayViewSet(ModelViewSet):
|
||||||
queryset = DeviceBay.objects.select_related('installed_device')
|
queryset = DeviceBay.objects.select_related('installed_device')
|
||||||
serializer_class = serializers.DeviceBaySerializer
|
serializer_class = serializers.DeviceBaySerializer
|
||||||
write_serializer_class = serializers.WritableDeviceBaySerializer
|
|
||||||
filter_class = filters.DeviceBayFilter
|
filter_class = filters.DeviceBayFilter
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(WritableSerializerMixin, ModelViewSet):
|
class InventoryItemViewSet(ModelViewSet):
|
||||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
serializer_class = serializers.InventoryItemSerializer
|
serializer_class = serializers.InventoryItemSerializer
|
||||||
write_serializer_class = serializers.WritableInventoryItemSerializer
|
|
||||||
filter_class = filters.InventoryItemFilter
|
filter_class = filters.InventoryItemFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -366,13 +379,21 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
|||||||
filter_class = filters.PowerConnectionFilter
|
filter_class = filters.PowerConnectionFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
|
class InterfaceConnectionViewSet(ModelViewSet):
|
||||||
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
||||||
serializer_class = serializers.InterfaceConnectionSerializer
|
serializer_class = serializers.InterfaceConnectionSerializer
|
||||||
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
|
|
||||||
filter_class = filters.InterfaceConnectionFilter
|
filter_class = filters.InterfaceConnectionFilter
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
|
queryset = VirtualChassis.objects.all()
|
||||||
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
#
|
#
|
||||||
@@ -387,14 +408,20 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
* `peer-interface`: The name of the peer interface
|
* `peer-interface`: The name of the peer interface
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
_device_param = Parameter('peer-device', 'query',
|
||||||
|
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
|
||||||
|
_interface_param = Parameter('peer-interface', 'query',
|
||||||
|
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Connected Device Locator"
|
return "Connected Device Locator"
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
peer_device_name = request.query_params.get('peer-device')
|
peer_device_name = request.query_params.get(self._device_param.name)
|
||||||
peer_interface_name = request.query_params.get('peer-interface')
|
peer_interface_name = request.query_params.get(self._interface_param.name)
|
||||||
if not peer_device_name or not peer_interface_name:
|
if not peer_device_name or not peer_interface_name:
|
||||||
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,12 @@ from django.apps import AppConfig
|
|||||||
class DCIMConfig(AppConfig):
|
class DCIMConfig(AppConfig):
|
||||||
name = "dcim"
|
name = "dcim"
|
||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
|
||||||
|
import dcim.signals
|
||||||
|
|
||||||
|
# register webhook signals
|
||||||
|
from extras.webhooks import register_signals
|
||||||
|
from .models import Site, Rack, RackGroup, Device, Interface
|
||||||
|
register_signals([Site, Rack, Device, Interface, RackGroup])
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ IFACE_FF_10GE_X2 = 1320
|
|||||||
IFACE_FF_25GE_SFP28 = 1350
|
IFACE_FF_25GE_SFP28 = 1350
|
||||||
IFACE_FF_40GE_QSFP_PLUS = 1400
|
IFACE_FF_40GE_QSFP_PLUS = 1400
|
||||||
IFACE_FF_100GE_CFP = 1500
|
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
|
IFACE_FF_100GE_QSFP28 = 1600
|
||||||
# Wireless
|
# Wireless
|
||||||
IFACE_FF_80211A = 2600
|
IFACE_FF_80211A = 2600
|
||||||
@@ -99,7 +102,7 @@ IFACE_FF_CHOICES = [
|
|||||||
[
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Ethernet (fixed)',
|
'Ethernet (fixed)',
|
||||||
@@ -122,6 +125,9 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
||||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
[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)'],
|
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@@ -187,24 +193,43 @@ WIRELESS_IFACE_TYPES = [
|
|||||||
|
|
||||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||||
|
|
||||||
# Device statuses
|
IFACE_MODE_ACCESS = 100
|
||||||
STATUS_OFFLINE = 0
|
IFACE_MODE_TAGGED = 200
|
||||||
STATUS_ACTIVE = 1
|
IFACE_MODE_TAGGED_ALL = 300
|
||||||
STATUS_PLANNED = 2
|
IFACE_MODE_CHOICES = [
|
||||||
STATUS_STAGED = 3
|
[IFACE_MODE_ACCESS, 'Access'],
|
||||||
STATUS_FAILED = 4
|
[IFACE_MODE_TAGGED, 'Tagged'],
|
||||||
STATUS_INVENTORY = 5
|
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
|
||||||
STATUS_CHOICES = [
|
|
||||||
[STATUS_ACTIVE, 'Active'],
|
|
||||||
[STATUS_OFFLINE, 'Offline'],
|
|
||||||
[STATUS_PLANNED, 'Planned'],
|
|
||||||
[STATUS_STAGED, 'Staged'],
|
|
||||||
[STATUS_FAILED, 'Failed'],
|
|
||||||
[STATUS_INVENTORY, 'Inventory'],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Bootstrap CSS classes for device stasuses
|
# Device statuses
|
||||||
DEVICE_STATUS_CLASSES = {
|
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',
|
0: 'warning',
|
||||||
1: 'success',
|
1: 'success',
|
||||||
2: 'info',
|
2: 'info',
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
|
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 (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||||
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionFilter(django_filters.FilterSet):
|
class RegionFilter(django_filters.FilterSet):
|
||||||
parent_id = NullableModelMultipleChoiceFilter(
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Parent region (ID)',
|
label='Parent region (ID)',
|
||||||
)
|
)
|
||||||
parent = NullableModelMultipleChoiceFilter(
|
parent = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='parent__slug',
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Parent region (slug)',
|
label='Parent region (slug)',
|
||||||
@@ -34,6 +42,15 @@ class RegionFilter(django_filters.FilterSet):
|
|||||||
model = Region
|
model = Region
|
||||||
fields = ['name', 'slug']
|
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):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
@@ -41,24 +58,33 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
region_id = NullableModelMultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=SITE_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Region (ID)',
|
label='Region (ID)',
|
||||||
)
|
)
|
||||||
region = NullableModelMultipleChoiceFilter(
|
region = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='region__slug',
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Region (slug)',
|
label='Region (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = NullableModelMultipleChoiceFilter(
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant__slug',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
@@ -70,6 +96,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
qs_filter = (
|
qs_filter = (
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(facility__icontains=value) |
|
Q(facility__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
Q(physical_address__icontains=value) |
|
Q(physical_address__icontains=value) |
|
||||||
Q(shipping_address__icontains=value) |
|
Q(shipping_address__icontains=value) |
|
||||||
Q(contact_name__icontains=value) |
|
Q(contact_name__icontains=value) |
|
||||||
@@ -125,40 +152,43 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = NullableModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
group = NullableModelMultipleChoiceFilter(
|
group = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group__slug',
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = NullableModelMultipleChoiceFilter(
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant__slug',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
role_id = NullableModelMultipleChoiceFilter(
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = NullableModelMultipleChoiceFilter(
|
role = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role__slug',
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['type', 'width', 'u_height', 'desc_units']
|
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -166,6 +196,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(facility_id__icontains=value) |
|
Q(facility_id__icontains=value) |
|
||||||
|
Q(serial__icontains=value.strip()) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -191,17 +222,27 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = NullableModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__group',
|
name='rack__group',
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
group = NullableModelMultipleChoiceFilter(
|
group = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__group',
|
name='rack__group__slug',
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
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(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
label='User (ID)',
|
label='User (ID)',
|
||||||
@@ -251,6 +292,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
@@ -324,10 +368,21 @@ class DeviceRoleFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['name', 'slug', 'color']
|
fields = ['name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class PlatformFilter(django_filters.FilterSet):
|
class PlatformFilter(django_filters.FilterSet):
|
||||||
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='manufacturer',
|
||||||
|
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:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
@@ -366,22 +421,22 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = NullableModelMultipleChoiceFilter(
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant__slug',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
platform_id = NullableModelMultipleChoiceFilter(
|
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
label='Platform (ID)',
|
label='Platform (ID)',
|
||||||
)
|
)
|
||||||
platform = NullableModelMultipleChoiceFilter(
|
platform = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='platform',
|
name='platform__slug',
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Platform (slug)',
|
label='Platform (slug)',
|
||||||
@@ -403,11 +458,15 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Rack group (ID)',
|
label='Rack group (ID)',
|
||||||
)
|
)
|
||||||
rack_id = NullableModelMultipleChoiceFilter(
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack',
|
name='rack',
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label='Rack (ID)',
|
label='Rack (ID)',
|
||||||
)
|
)
|
||||||
|
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
label='VM cluster (ID)',
|
||||||
|
)
|
||||||
model = django_filters.ModelMultipleChoiceFilter(
|
model = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type__slug',
|
name='device_type__slug',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
@@ -415,7 +474,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Device model (slug)',
|
label='Device model (slug)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=STATUS_CHOICES
|
choices=DEVICE_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
)
|
)
|
||||||
is_full_depth = django_filters.BooleanFilter(
|
is_full_depth = django_filters.BooleanFilter(
|
||||||
name='device_type__is_full_depth',
|
name='device_type__is_full_depth',
|
||||||
@@ -441,10 +501,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
method='_has_primary_ip',
|
method='_has_primary_ip',
|
||||||
label='Has a primary IP',
|
label='Has a primary IP',
|
||||||
)
|
)
|
||||||
|
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='virtual_chassis',
|
||||||
|
queryset=VirtualChassis.objects.all(),
|
||||||
|
label='Virtual chassis (ID)',
|
||||||
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['serial']
|
fields = ['serial', 'position']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -490,6 +558,9 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||||
@@ -548,6 +619,9 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
method='_mac_address',
|
method='_mac_address',
|
||||||
label='MAC address',
|
label='MAC address',
|
||||||
)
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@@ -556,8 +630,9 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
try:
|
try:
|
||||||
device = Device.objects.select_related('device_type').get(**{name: value})
|
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
|
ordering = device.device_type.interface_ordering
|
||||||
return queryset.filter(device=device).order_naturally(ordering)
|
return queryset.filter(pk__in=vc_interface_ids).order_naturally(ordering)
|
||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
@@ -589,7 +664,11 @@ class DeviceBayFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||||
parent_id = NullableModelMultipleChoiceFilter(
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
label='Parent inventory item (ID)',
|
label='Parent inventory item (ID)',
|
||||||
)
|
)
|
||||||
@@ -607,7 +686,64 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'part_id', 'serial', 'discovered']
|
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):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='master__site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site (ID)',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='master__site__slug',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site name (slug)',
|
||||||
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='master__tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='master__tenant__slug',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
tag = django_filters.CharFilter(
|
||||||
|
name='tags__slug',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['domain']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(master__name__icontains=value) |
|
||||||
|
Q(domain__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
|
|||||||
@@ -70,6 +70,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 1,
|
"manufacturer": 1,
|
||||||
"model": "MX960",
|
"model": "MX960",
|
||||||
"slug": "mx960",
|
"slug": "mx960",
|
||||||
@@ -84,6 +86,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 2,
|
"pk": 2,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 1,
|
"manufacturer": 1,
|
||||||
"model": "EX9214",
|
"model": "EX9214",
|
||||||
"slug": "ex9214",
|
"slug": "ex9214",
|
||||||
@@ -98,6 +102,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 3,
|
"pk": 3,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 1,
|
"manufacturer": 1,
|
||||||
"model": "QFX5100-24Q",
|
"model": "QFX5100-24Q",
|
||||||
"slug": "qfx5100-24q",
|
"slug": "qfx5100-24q",
|
||||||
@@ -112,6 +118,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 4,
|
"pk": 4,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 1,
|
"manufacturer": 1,
|
||||||
"model": "QFX5100-48S",
|
"model": "QFX5100-48S",
|
||||||
"slug": "qfx5100-48s",
|
"slug": "qfx5100-48s",
|
||||||
@@ -126,6 +134,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 5,
|
"pk": 5,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 2,
|
"manufacturer": 2,
|
||||||
"model": "CM4148",
|
"model": "CM4148",
|
||||||
"slug": "cm4148",
|
"slug": "cm4148",
|
||||||
@@ -140,6 +150,8 @@
|
|||||||
"model": "dcim.devicetype",
|
"model": "dcim.devicetype",
|
||||||
"pk": 6,
|
"pk": 6,
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"created": "2016-06-23",
|
||||||
|
"last_updated": "2016-06-23T03:19:56.521Z",
|
||||||
"manufacturer": 3,
|
"manufacturer": 3,
|
||||||
"model": "CWG-24VYM415C9",
|
"model": "CWG-24VYM415C9",
|
||||||
"slug": "cwg-24vym415c9",
|
"slug": "cwg-24vym415c9",
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, AddrFormatError
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from netaddr import EUI, AddrFormatError
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
netbox/dcim/migrations/0055_virtualchassis_ordering.py
Normal file
25
netbox/dcim/migrations/0055_virtualchassis_ordering.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-21 14:41
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0054_site_status_timezone_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='virtualchassis',
|
||||||
|
options={'ordering': ['master'], 'verbose_name_plural': 'virtual chassis'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='master',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
netbox/dcim/migrations/0056_django2.py
Normal file
24
netbox/dcim/migrations/0056_django2.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 2.0.3 on 2018-03-30 14:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0055_virtualchassis_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='untagged_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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.PROTECT, related_name='platforms', to='dcim.Manufacturer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
77
netbox/dcim/migrations/0057_tags.py
Normal file
77
netbox/dcim/migrations/0057_tags.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('taggit', '0002_auto_20150616_2121'),
|
||||||
|
('dcim', '0056_django2'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
netbox/dcim/migrations/0058_relax_rack_naming_constraints.py
Normal file
23
netbox/dcim/migrations/0058_relax_rack_naming_constraints.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-22 19:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0057_tags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rack',
|
||||||
|
options={'ordering': ['site', 'group', 'name']},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='rack',
|
||||||
|
unique_together=set([('group', 'name'), ('group', 'facility_id')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
netbox/dcim/migrations/0059_site_latitude_longitude.py
Normal file
25
netbox/dcim/migrations/0059_site_latitude_longitude.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-06-21 18:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0058_relax_rack_naming_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
135
netbox/dcim/migrations/0060_change_logging.py
Normal file
135
netbox/dcim/migrations/0060_change_logging.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-06-13 17:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0059_site_latitude_longitude'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicerole',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicerole',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='manufacturer',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='manufacturer',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackgroup',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackgroup',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackreservation',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackrole',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackrole',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='region',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='region',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rackreservation',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
netbox/dcim/migrations/0061_platform_napalm_args.py
Normal file
19
netbox/dcim/migrations/0061_platform_napalm_args.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.6 on 2018-06-29 15:02
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0060_change_logging'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='napalm_args',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'),
|
||||||
|
),
|
||||||
|
]
|
||||||
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]+)(\d{{1,9}})$') AS integer)"
|
||||||
|
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)"
|
||||||
|
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)"
|
||||||
|
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)"
|
||||||
|
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)"
|
||||||
|
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
||||||
|
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,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)
|
||||||
28
netbox/dcim/signals.py
Normal file
28
netbox/dcim/signals.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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.
|
||||||
|
"""
|
||||||
|
# Default to 1 but don't overwrite an existing position (see #2087)
|
||||||
|
if instance.master.vc_position is not None:
|
||||||
|
vc_position = instance.master.vc_position
|
||||||
|
else:
|
||||||
|
vc_position = 1
|
||||||
|
if created:
|
||||||
|
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
@@ -3,14 +3,15 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from tenancy.tables import COL_TENANT
|
||||||
|
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
|
RackReservation, Region, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
REGION_LINK = """
|
REGION_LINK = """
|
||||||
{% if record.get_children %}
|
{% if record.get_children %}
|
||||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
||||||
@@ -40,18 +41,32 @@ DEVICE_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
REGION_ACTIONS = """
|
REGION_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_region %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RACKGROUP_ACTIONS = """
|
RACKGROUP_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_rackgroup %}
|
{% 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>
|
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
|
||||||
|
<i class="glyphicon glyphicon-pencil"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RACKROLE_ACTIONS = """
|
RACKROLE_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_rackrole %}
|
{% if perms.dcim.change_rackrole %}
|
||||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -65,25 +80,57 @@ RACK_ROLE = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
RACK_DEVICE_COUNT = """
|
||||||
|
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
|
||||||
|
"""
|
||||||
|
|
||||||
RACKRESERVATION_ACTIONS = """
|
RACKRESERVATION_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_rackreservation %}
|
{% 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>
|
<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 %}
|
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
MANUFACTURER_ACTIONS = """
|
MANUFACTURER_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_manufacturer %}
|
{% if perms.dcim.change_manufacturer %}
|
||||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEVICEROLE_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
|
{% 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>
|
||||||
|
{% 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 = """
|
PLATFORM_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.dcim.change_platform %}
|
{% 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>
|
<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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -93,7 +140,7 @@ DEVICE_ROLE = """
|
|||||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEVICE_STATUS = """
|
STATUS_LABEL = """
|
||||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -107,11 +154,24 @@ SUBDEVICE_ROLE_TEMPLATE = """
|
|||||||
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEVICETYPE_INSTANCES_TEMPLATE = """
|
||||||
|
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
|
||||||
|
"""
|
||||||
|
|
||||||
UTILIZATION_GRAPH = """
|
UTILIZATION_GRAPH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% utilization_graph value %}
|
{% utilization_graph value %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
VIRTUALCHASSIS_ACTIONS = """
|
||||||
|
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
|
{% 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
|
# Regions
|
||||||
@@ -139,27 +199,14 @@ class RegionTable(BaseTable):
|
|||||||
|
|
||||||
class SiteTable(BaseTable):
|
class SiteTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
||||||
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
|
fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
|
||||||
|
|
||||||
|
|
||||||
class SiteDetailTable(SiteTable):
|
|
||||||
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')
|
|
||||||
|
|
||||||
class Meta(SiteTable.Meta):
|
|
||||||
fields = (
|
|
||||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
|
||||||
'vlan_count', 'circuit_count',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -168,12 +215,21 @@ class SiteDetailTable(SiteTable):
|
|||||||
|
|
||||||
class RackGroupTable(BaseTable):
|
class RackGroupTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn()
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn(
|
||||||
rack_count = tables.Column(verbose_name='Racks')
|
viewname='dcim:site',
|
||||||
slug = tables.Column(verbose_name='Slug')
|
args=[Accessor('site.slug')],
|
||||||
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
verbose_name='Site'
|
||||||
verbose_name='')
|
)
|
||||||
|
rack_count = tables.Column(
|
||||||
|
verbose_name='Racks'
|
||||||
|
)
|
||||||
|
slug = tables.Column()
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=RACKGROUP_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
@@ -204,10 +260,10 @@ class RackRoleTable(BaseTable):
|
|||||||
|
|
||||||
class RackTable(BaseTable):
|
class RackTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
role = tables.TemplateColumn(RACK_ROLE)
|
role = tables.TemplateColumn(RACK_ROLE)
|
||||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||||
|
|
||||||
@@ -217,12 +273,16 @@ class RackTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class RackDetailTable(RackTable):
|
class RackDetailTable(RackTable):
|
||||||
devices = tables.Column(accessor=Accessor('device_count'))
|
device_count = tables.TemplateColumn(
|
||||||
|
template_code=RACK_DEVICE_COUNT,
|
||||||
|
verbose_name='Devices'
|
||||||
|
)
|
||||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
|
|
||||||
class Meta(RackTable.Meta):
|
class Meta(RackTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||||
|
'get_utilization',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -231,7 +291,7 @@ class RackImportTable(BaseTable):
|
|||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
facility_id = tables.Column(verbose_name='Facility ID')
|
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)')
|
u_height = tables.Column(verbose_name='Height (U)')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -245,6 +305,7 @@ class RackImportTable(BaseTable):
|
|||||||
|
|
||||||
class RackReservationTable(BaseTable):
|
class RackReservationTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
@@ -253,7 +314,7 @@ class RackReservationTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
|
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -264,13 +325,14 @@ class ManufacturerTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||||
|
platform_count = tables.Column(verbose_name='Platforms')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
verbose_name='')
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
|
fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -279,13 +341,23 @@ class ManufacturerTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceTypeTable(BaseTable):
|
class DeviceTypeTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
model = tables.LinkColumn(
|
||||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
viewname='dcim:devicetype',
|
||||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
args=[Accessor('pk')],
|
||||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
verbose_name='Device Type'
|
||||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
)
|
||||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
is_full_depth = BooleanColumn(verbose_name='Full Depth')
|
||||||
instance_count = tables.Column(verbose_name='Instances')
|
is_console_server = BooleanColumn(verbose_name='CS')
|
||||||
|
is_pdu = BooleanColumn(verbose_name='PDU')
|
||||||
|
is_network_device = BooleanColumn(verbose_name='Net')
|
||||||
|
subdevice_role = tables.TemplateColumn(
|
||||||
|
template_code=SUBDEVICE_ROLE_TEMPLATE,
|
||||||
|
verbose_name='Subdevice Role'
|
||||||
|
)
|
||||||
|
instance_count = tables.TemplateColumn(
|
||||||
|
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
|
||||||
|
verbose_name='Instances'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
@@ -361,15 +433,29 @@ class DeviceBayTemplateTable(BaseTable):
|
|||||||
class DeviceRoleTable(BaseTable):
|
class DeviceRoleTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
device_count = tables.Column(verbose_name='Devices')
|
device_count = tables.TemplateColumn(
|
||||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
|
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')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
actions = tables.TemplateColumn(
|
||||||
verbose_name='')
|
template_code=DEVICEROLE_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
|
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -378,16 +464,27 @@ class DeviceRoleTable(BaseTable):
|
|||||||
|
|
||||||
class PlatformTable(BaseTable):
|
class PlatformTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
device_count = tables.TemplateColumn(
|
||||||
device_count = tables.Column(verbose_name='Devices')
|
template_code=PLATFORM_DEVICE_COUNT,
|
||||||
slug = tables.Column(verbose_name='Slug')
|
accessor=Accessor('devices.count'),
|
||||||
rpc_client = tables.Column(accessor='get_rpc_client_display', orderable=False, verbose_name='RPC Client')
|
orderable=False,
|
||||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
verbose_name='Devices'
|
||||||
verbose_name='')
|
)
|
||||||
|
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):
|
class Meta(BaseTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ('pk', 'name', 'device_count', 'slug', 'rpc_client', 'actions')
|
fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -396,9 +493,12 @@ class PlatformTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceTable(BaseTable):
|
class DeviceTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
name = tables.TemplateColumn(
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
order_by=('_nat1', '_nat2', '_nat3'),
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
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')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||||
@@ -424,8 +524,8 @@ class DeviceDetailTable(DeviceTable):
|
|||||||
|
|
||||||
class DeviceImportTable(BaseTable):
|
class DeviceImportTable(BaseTable):
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||||
position = tables.Column(verbose_name='Position')
|
position = tables.Column(verbose_name='Position')
|
||||||
@@ -521,5 +621,38 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
interface_b = tables.Column(verbose_name='Interface B')
|
interface_b = tables.Column(verbose_name='Interface B')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = InterfaceConnection
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
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')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ class DeviceTestCase(TestCase):
|
|||||||
'face': RACK_FACE_FRONT,
|
'face': RACK_FACE_FRONT,
|
||||||
'position': 41,
|
'position': 41,
|
||||||
'platform': get_id(Platform, 'juniper-junos'),
|
'platform': get_id(Platform, 'juniper-junos'),
|
||||||
'status': STATUS_ACTIVE,
|
'status': DEVICE_STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
||||||
self.assertTrue(test.save())
|
self.assertTrue(test.save())
|
||||||
@@ -43,7 +43,7 @@ class DeviceTestCase(TestCase):
|
|||||||
'face': RACK_FACE_FRONT,
|
'face': RACK_FACE_FRONT,
|
||||||
'position': 1,
|
'position': 1,
|
||||||
'platform': get_id(Platform, 'juniper-junos'),
|
'platform': get_id(Platform, 'juniper-junos'),
|
||||||
'status': STATUS_ACTIVE,
|
'status': DEVICE_STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertFalse(test.is_valid())
|
self.assertFalse(test.is_valid())
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class DeviceTestCase(TestCase):
|
|||||||
'face': None,
|
'face': None,
|
||||||
'position': None,
|
'position': None,
|
||||||
'platform': None,
|
'platform': None,
|
||||||
'status': STATUS_ACTIVE,
|
'status': DEVICE_STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid())
|
self.assertTrue(test.is_valid())
|
||||||
self.assertTrue(test.save())
|
self.assertTrue(test.save())
|
||||||
@@ -76,7 +76,7 @@ class DeviceTestCase(TestCase):
|
|||||||
'face': RACK_FACE_REAR,
|
'face': RACK_FACE_REAR,
|
||||||
'position': None,
|
'position': None,
|
||||||
'platform': None,
|
'platform': None,
|
||||||
'status': STATUS_ACTIVE,
|
'status': DEVICE_STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid())
|
self.assertTrue(test.is_valid())
|
||||||
self.assertTrue(test.save())
|
self.assertTrue(test.save())
|
||||||
|
|||||||
@@ -9,14 +9,29 @@ class RackTestCase(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
self.site = Site.objects.create(
|
self.site1 = Site.objects.create(
|
||||||
name='TestSite1',
|
name='TestSite1',
|
||||||
slug='my-test-site'
|
slug='test-site-1'
|
||||||
|
)
|
||||||
|
self.site2 = Site.objects.create(
|
||||||
|
name='TestSite2',
|
||||||
|
slug='test-site-2'
|
||||||
|
)
|
||||||
|
self.group1 = RackGroup.objects.create(
|
||||||
|
name='TestGroup1',
|
||||||
|
slug='test-group-1',
|
||||||
|
site=self.site1
|
||||||
|
)
|
||||||
|
self.group2 = RackGroup.objects.create(
|
||||||
|
name='TestGroup2',
|
||||||
|
slug='test-group-2',
|
||||||
|
site=self.site2
|
||||||
)
|
)
|
||||||
self.rack = Rack.objects.create(
|
self.rack = Rack.objects.create(
|
||||||
name='TestRack1',
|
name='TestRack1',
|
||||||
facility_id='A101',
|
facility_id='A101',
|
||||||
site=self.site,
|
site=self.site1,
|
||||||
|
group=self.group1,
|
||||||
u_height=42
|
u_height=42
|
||||||
)
|
)
|
||||||
self.manufacturer = Manufacturer.objects.create(
|
self.manufacturer = Manufacturer.objects.create(
|
||||||
@@ -57,13 +72,51 @@ class RackTestCase(TestCase):
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_rack_device_outside_height(self):
|
||||||
|
|
||||||
|
rack1 = Rack(
|
||||||
|
name='TestRack2',
|
||||||
|
facility_id='A102',
|
||||||
|
site=self.site1,
|
||||||
|
u_height=42
|
||||||
|
)
|
||||||
|
rack1.save()
|
||||||
|
|
||||||
|
device1 = Device(
|
||||||
|
name='TestSwitch1',
|
||||||
|
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||||
|
device_role=DeviceRole.objects.get(slug='switch'),
|
||||||
|
site=self.site1,
|
||||||
|
rack=rack1,
|
||||||
|
position=43,
|
||||||
|
face=RACK_FACE_FRONT,
|
||||||
|
)
|
||||||
|
device1.save()
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
rack1.clean()
|
||||||
|
|
||||||
|
def test_rack_group_site(self):
|
||||||
|
|
||||||
|
rack_invalid_group = Rack(
|
||||||
|
name='TestRack2',
|
||||||
|
facility_id='A102',
|
||||||
|
site=self.site1,
|
||||||
|
u_height=42,
|
||||||
|
group=self.group2
|
||||||
|
)
|
||||||
|
rack_invalid_group.save()
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
rack_invalid_group.clean()
|
||||||
|
|
||||||
def test_mount_single_device(self):
|
def test_mount_single_device(self):
|
||||||
|
|
||||||
device1 = Device(
|
device1 = Device(
|
||||||
name='TestSwitch1',
|
name='TestSwitch1',
|
||||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||||
device_role=DeviceRole.objects.get(slug='switch'),
|
device_role=DeviceRole.objects.get(slug='switch'),
|
||||||
site=self.site,
|
site=self.site1,
|
||||||
rack=self.rack,
|
rack=self.rack,
|
||||||
position=10,
|
position=10,
|
||||||
face=RACK_FACE_REAR,
|
face=RACK_FACE_REAR,
|
||||||
@@ -92,7 +145,7 @@ class RackTestCase(TestCase):
|
|||||||
name='TestPDU',
|
name='TestPDU',
|
||||||
device_role=self.role.get('PDU'),
|
device_role=self.role.get('PDU'),
|
||||||
device_type=self.device_type.get('cc5000'),
|
device_type=self.device_type.get('cc5000'),
|
||||||
site=self.site,
|
site=self.site1,
|
||||||
rack=self.rack,
|
rack=self.rack,
|
||||||
position=None,
|
position=None,
|
||||||
face=None,
|
face=None,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from extras.views import ImageAttachmentEditView
|
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||||
from ipam.views import ServiceCreateView
|
from ipam.views import ServiceCreateView
|
||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
from .models import Device, Rack, Site
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from .models import (
|
||||||
|
Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
|
VirtualChassis,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = 'dcim'
|
app_name = 'dcim'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -15,8 +17,10 @@ urlpatterns = [
|
|||||||
# Regions
|
# Regions
|
||||||
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||||
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
|
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
|
||||||
|
url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||||
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||||
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||||
|
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||||
@@ -26,25 +30,32 @@ urlpatterns = [
|
|||||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||||
|
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||||
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||||
|
|
||||||
# Rack groups
|
# Rack groups
|
||||||
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||||
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||||
|
url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||||
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||||
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||||
|
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||||
|
|
||||||
# Rack roles
|
# Rack roles
|
||||||
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||||
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||||
|
url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||||
|
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||||
|
|
||||||
# Rack reservations
|
# Rack reservations
|
||||||
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||||
|
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||||
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||||
@@ -56,23 +67,28 @@ urlpatterns = [
|
|||||||
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
||||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
|
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||||
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||||
|
url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||||
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||||
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||||
|
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||||
|
|
||||||
# Device types
|
# Device types
|
||||||
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||||
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||||
|
url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||||
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||||
|
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||||
|
|
||||||
# Console port templates
|
# Console port templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||||
@@ -102,14 +118,18 @@ urlpatterns = [
|
|||||||
# Device roles
|
# Device roles
|
||||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||||
|
url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||||
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||||
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||||
|
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
||||||
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
|
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||||
|
url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||||
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||||
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||||
|
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||||
|
|
||||||
# Devices
|
# Devices
|
||||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||||
@@ -121,20 +141,22 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
||||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||||
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
||||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
||||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
|
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.ConsolePortConnectView.as_view(), name='consoleport_connect'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.ConsolePortDisconnectView.as_view(), name='consoleport_disconnect'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||||
|
|
||||||
@@ -143,17 +165,18 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.ConsoleServerPortConnectView.as_view(), name='consoleserverport_connect'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.ConsoleServerPortDisconnectView.as_view(), name='consoleserverport_disconnect'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||||
|
url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||||
|
|
||||||
# Power ports
|
# Power ports
|
||||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.PowerPortConnectView.as_view(), name='powerport_connect'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.PowerPortDisconnectView.as_view(), name='powerport_disconnect'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||||
|
|
||||||
@@ -162,10 +185,11 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.PowerOutletConnectView.as_view(), name='poweroutlet_connect'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.PowerOutletDisconnectView.as_view(), name='poweroutlet_disconnect'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||||
|
url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||||
@@ -173,10 +197,13 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
|
||||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
|
||||||
|
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||||
|
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||||
|
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||||
|
|
||||||
# Device bays
|
# Device bays
|
||||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||||
@@ -184,13 +211,18 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||||
|
url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||||
|
|
||||||
# Inventory items
|
# Inventory items
|
||||||
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||||
|
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||||
|
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||||
|
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||||
|
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||||
|
|
||||||
# Console/power/interface connections
|
# Console/power/interface connections
|
||||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||||
@@ -200,4 +232,13 @@ urlpatterns = [
|
|||||||
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||||
|
|
||||||
|
# Virtual chassis
|
||||||
|
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||||
|
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||||
|
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||||
|
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||||
|
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||||
|
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||||
|
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
1389
netbox/dcim/views.py
1389
netbox/dcim/views.py
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = 'extras.apps.ExtrasConfig'
|
||||||
|
|
||||||
|
# check that django-rq is installed and we can connect to redis
|
||||||
|
if settings.WEBHOOKS_ENABLED:
|
||||||
|
try:
|
||||||
|
import django_rq
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"django-rq is not installed! You must install this package per "
|
||||||
|
"the documentation to use the webhook backend."
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,9 +4,57 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
|
from utilities.forms import LaxURLField
|
||||||
|
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||||
|
from .models import (
|
||||||
|
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
|
||||||
|
Webhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def order_content_types(field):
|
||||||
|
"""
|
||||||
|
Order the list of available ContentTypes by application
|
||||||
|
"""
|
||||||
|
queryset = field.queryset.order_by('app_label', 'model')
|
||||||
|
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebhookForm(forms.ModelForm):
|
||||||
|
payload_url = LaxURLField(
|
||||||
|
label='URL'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WebhookForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
order_content_types(self.fields['obj_type'])
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Webhook)
|
||||||
|
class WebhookAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
||||||
|
'type_delete', 'ssl_verification',
|
||||||
|
]
|
||||||
|
form = WebhookForm
|
||||||
|
|
||||||
|
def models(self, obj):
|
||||||
|
return ', '.join([ct.name for ct in obj.obj_type.all()])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
class CustomFieldForm(forms.ModelForm):
|
class CustomFieldForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -16,9 +64,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Organize the available ContentTypes
|
order_content_types(self.fields['obj_type'])
|
||||||
queryset = self.fields['obj_type'].queryset.order_by('app_label', 'model')
|
|
||||||
self.fields['obj_type'].choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceAdmin(admin.TabularInline):
|
class CustomFieldChoiceAdmin(admin.TabularInline):
|
||||||
@@ -29,23 +75,50 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
|||||||
@admin.register(CustomField)
|
@admin.register(CustomField)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
inlines = [CustomFieldChoiceAdmin]
|
inlines = [CustomFieldChoiceAdmin]
|
||||||
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
|
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||||
form = CustomFieldForm
|
form = CustomFieldForm
|
||||||
|
|
||||||
def models(self, obj):
|
def models(self, obj):
|
||||||
return ', '.join([ct.name for ct in obj.obj_type.all()])
|
return ', '.join([ct.name for ct in obj.obj_type.all()])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Graphs
|
||||||
|
#
|
||||||
|
|
||||||
@admin.register(Graph)
|
@admin.register(Graph)
|
||||||
class GraphAdmin(admin.ModelAdmin):
|
class GraphAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'type', 'weight', 'source']
|
list_display = ['name', 'type', 'weight', 'source']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Export templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExportTemplateForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExportTemplate
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ExportTemplateForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Format ContentType choices
|
||||||
|
order_content_types(self.fields['content_type'])
|
||||||
|
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ExportTemplate)
|
@admin.register(ExportTemplate)
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
||||||
|
form = ExportTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Topology maps
|
||||||
|
#
|
||||||
|
|
||||||
@admin.register(TopologyMap)
|
@admin.register(TopologyMap)
|
||||||
class TopologyMapAdmin(admin.ModelAdmin):
|
class TopologyMapAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'slug', 'site']
|
list_display = ['name', 'slug', 'site']
|
||||||
@@ -54,6 +127,62 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config contexts
|
||||||
|
#
|
||||||
|
|
||||||
|
@admin.register(ConfigContext)
|
||||||
|
class ConfigContextAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'weight']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging
|
||||||
|
#
|
||||||
|
|
||||||
|
@admin.register(ObjectChange)
|
||||||
|
class ObjectChangeAdmin(admin.ModelAdmin):
|
||||||
|
actions = None
|
||||||
|
fields = ['time', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
|
||||||
|
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
|
||||||
|
list_filter = ['time', 'action', 'user__username']
|
||||||
|
list_select_related = ['changed_object_type', 'user']
|
||||||
|
readonly_fields = fields
|
||||||
|
search_fields = ['user_name', 'object_repr', 'request_id']
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def display_user(self, obj):
|
||||||
|
if obj.user is not None:
|
||||||
|
return obj.user
|
||||||
|
else:
|
||||||
|
return '{} (deleted)'.format(obj.user_name)
|
||||||
|
display_user.short_description = 'user'
|
||||||
|
|
||||||
|
def display_action(self, obj):
|
||||||
|
icon = {
|
||||||
|
OBJECTCHANGE_ACTION_CREATE: 'addlink',
|
||||||
|
OBJECTCHANGE_ACTION_UPDATE: 'changelink',
|
||||||
|
OBJECTCHANGE_ACTION_DELETE: 'deletelink',
|
||||||
|
}
|
||||||
|
return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
|
||||||
|
display_action.short_description = 'action'
|
||||||
|
|
||||||
|
def display_object(self, obj):
|
||||||
|
if hasattr(obj.changed_object, 'get_absolute_url'):
|
||||||
|
return mark_safe('<a href="{}">{}</a>'.format(obj.changed_object.get_absolute_url(), obj.changed_object))
|
||||||
|
elif obj.changed_object is not None:
|
||||||
|
return obj.changed_object
|
||||||
|
else:
|
||||||
|
return '{} (deleted)'.format(obj.object_repr)
|
||||||
|
display_object.short_description = 'object'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# User actions
|
||||||
|
#
|
||||||
|
|
||||||
@admin.register(UserAction)
|
@admin.register(UserAction)
|
||||||
class UserActionAdmin(admin.ModelAdmin):
|
class UserActionAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from datetime import datetime
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from extras.models import (
|
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
|
||||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||||
)
|
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import ValidatedModelSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
|||||||
# Data validation
|
# Data validation
|
||||||
if value not in [None, '']:
|
if value not in [None, '']:
|
||||||
|
|
||||||
|
# Validate integer
|
||||||
|
if cf.type == CF_TYPE_INTEGER:
|
||||||
|
try:
|
||||||
|
int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(
|
||||||
|
"Invalid value for integer field {}: {}".format(field_name, value)
|
||||||
|
)
|
||||||
|
|
||||||
# Validate boolean
|
# Validate boolean
|
||||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
from dcim.api.serializers import (
|
||||||
|
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||||
|
NestedRegionSerializer, NestedSiteSerializer,
|
||||||
|
)
|
||||||
from dcim.models import Device, Rack, Site
|
from dcim.models import Device, Rack, Site
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
|
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||||
)
|
)
|
||||||
|
from extras.constants import *
|
||||||
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from users.api.serializers import NestedUserSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
from utilities.api import (
|
||||||
|
ChoiceFieldSerializer, ContentTypeFieldSerializer, get_serializer_for_model, ValidatedModelSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Graphs
|
# Graphs
|
||||||
#
|
#
|
||||||
|
|
||||||
class GraphSerializer(serializers.ModelSerializer):
|
class GraphSerializer(ValidatedModelSerializer):
|
||||||
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -25,13 +32,6 @@ class GraphSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
||||||
|
|
||||||
|
|
||||||
class WritableGraphSerializer(serializers.ModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
|
|
||||||
|
|
||||||
|
|
||||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||||
embed_url = serializers.SerializerMethodField()
|
embed_url = serializers.SerializerMethodField()
|
||||||
embed_link = serializers.SerializerMethodField()
|
embed_link = serializers.SerializerMethodField()
|
||||||
@@ -52,7 +52,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
|||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplateSerializer(serializers.ModelSerializer):
|
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
@@ -63,7 +63,7 @@ class ExportTemplateSerializer(serializers.ModelSerializer):
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
#
|
#
|
||||||
|
|
||||||
class TopologyMapSerializer(serializers.ModelSerializer):
|
class TopologyMapSerializer(ValidatedModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -71,23 +71,46 @@ class TopologyMapSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableTopologyMapSerializer(serializers.ModelSerializer):
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagSerializer(ValidatedModelSerializer):
|
||||||
|
tagged_items = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TopologyMap
|
model = Tag
|
||||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
fields = ['id', 'name', 'slug', 'tagged_items']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|
||||||
class ImageAttachmentSerializer(serializers.ModelSerializer):
|
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||||
parent = serializers.SerializerMethodField()
|
content_type = ContentTypeFieldSerializer()
|
||||||
|
parent = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
|
fields = [
|
||||||
|
'id', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created',
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate that the parent object exists
|
||||||
|
try:
|
||||||
|
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(ImageAttachmentSerializer, self).validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def get_parent(self, obj):
|
def get_parent(self, obj):
|
||||||
|
|
||||||
@@ -104,26 +127,86 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
|
|||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
|
||||||
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
|
#
|
||||||
content_type = ContentTypeFieldSerializer()
|
# Config contexts
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigContextSerializer(ValidatedModelSerializer):
|
||||||
|
regions = NestedRegionSerializer(required=False, many=True)
|
||||||
|
sites = NestedSiteSerializer(required=False, many=True)
|
||||||
|
roles = NestedDeviceRoleSerializer(required=False, many=True)
|
||||||
|
platforms = NestedPlatformSerializer(required=False, many=True)
|
||||||
|
tenants = NestedTenantSerializer(required=False, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ConfigContext
|
||||||
fields = ['id', 'content_type', 'object_id', 'name', 'image']
|
fields = [
|
||||||
|
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants',
|
||||||
|
'data',
|
||||||
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate that the parent object exists
|
#
|
||||||
try:
|
# Reports
|
||||||
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
#
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enforce model validation
|
class ReportResultSerializer(serializers.ModelSerializer):
|
||||||
super(WritableImageAttachmentSerializer, self).validate(data)
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReportResult
|
||||||
|
fields = ['created', 'user', 'failed', 'data']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(
|
||||||
|
view_name='extras-api:report-detail',
|
||||||
|
lookup_field='report',
|
||||||
|
lookup_url_kwarg='pk'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReportResult
|
||||||
|
fields = ['url', 'created', 'user', 'failed']
|
||||||
|
|
||||||
|
|
||||||
|
class ReportSerializer(serializers.Serializer):
|
||||||
|
module = serializers.CharField(max_length=255)
|
||||||
|
name = serializers.CharField(max_length=255)
|
||||||
|
description = serializers.CharField(max_length=255, required=False)
|
||||||
|
test_methods = serializers.ListField(child=serializers.CharField(max_length=255))
|
||||||
|
result = NestedReportResultSerializer()
|
||||||
|
|
||||||
|
|
||||||
|
class ReportDetailSerializer(ReportSerializer):
|
||||||
|
result = ReportResultSerializer()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||||
|
user = NestedUserSerializer(read_only=True)
|
||||||
|
content_type = ContentTypeFieldSerializer(read_only=True)
|
||||||
|
changed_object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectChange
|
||||||
|
fields = [
|
||||||
|
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_changed_object(self, obj):
|
||||||
|
"""
|
||||||
|
Serialize a nested representation of the changed object.
|
||||||
|
"""
|
||||||
|
if obj.changed_object is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
|
||||||
|
if serializer is None:
|
||||||
|
return obj.object_repr
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
data = serializer(obj.changed_object, context=context).data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class ExtrasRootView(routers.APIRootView):
|
|||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.APIRootView = ExtrasRootView
|
router.APIRootView = ExtrasRootView
|
||||||
|
|
||||||
|
# Field choices
|
||||||
|
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, base_name='field-choice')
|
||||||
|
|
||||||
# Graphs
|
# Graphs
|
||||||
router.register(r'graphs', views.GraphViewSet)
|
router.register(r'graphs', views.GraphViewSet)
|
||||||
|
|
||||||
@@ -25,9 +28,21 @@ router.register(r'export-templates', views.ExportTemplateViewSet)
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
router.register(r'topology-maps', views.TopologyMapViewSet)
|
router.register(r'topology-maps', views.TopologyMapViewSet)
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
router.register(r'tags', views.TagViewSet)
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||||
|
|
||||||
|
# Config contexts
|
||||||
|
router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||||
|
|
||||||
|
# Reports
|
||||||
|
router.register(r'reports', views.ReportViewSet, base_name='report')
|
||||||
|
|
||||||
|
# Change logging
|
||||||
|
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||||
|
|
||||||
# Recent activity
|
# Recent activity
|
||||||
router.register(r'recent-activity', views.RecentActivityViewSet)
|
router.register(r'recent-activity', views.RecentActivityViewSet)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import HttpResponse
|
from django.db.models import Count
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction
|
from extras.models import (
|
||||||
from utilities.api import WritableSerializerMixin
|
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||||
|
UserAction,
|
||||||
|
)
|
||||||
|
from extras.reports import get_report, get_reports
|
||||||
|
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Field choices
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
|
||||||
|
fields = (
|
||||||
|
(CustomField, ['type']),
|
||||||
|
(Graph, ['type']),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
class CustomFieldModelViewSet(ModelViewSet):
|
class CustomFieldModelViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Include the applicable set of CustomFields in the ModelViewSet context.
|
Include the applicable set of CustomFields in the ModelViewSet context.
|
||||||
@@ -43,26 +65,36 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|||||||
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
|
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
|
||||||
|
|
||||||
|
|
||||||
class GraphViewSet(WritableSerializerMixin, ModelViewSet):
|
#
|
||||||
|
# Graphs
|
||||||
|
#
|
||||||
|
|
||||||
|
class GraphViewSet(ModelViewSet):
|
||||||
queryset = Graph.objects.all()
|
queryset = Graph.objects.all()
|
||||||
serializer_class = serializers.GraphSerializer
|
serializer_class = serializers.GraphSerializer
|
||||||
write_serializer_class = serializers.WritableGraphSerializer
|
|
||||||
filter_class = filters.GraphFilter
|
filter_class = filters.GraphFilter
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
#
|
||||||
|
# Export templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExportTemplateViewSet(ModelViewSet):
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
serializer_class = serializers.ExportTemplateSerializer
|
serializer_class = serializers.ExportTemplateSerializer
|
||||||
filter_class = filters.ExportTemplateFilter
|
filter_class = filters.ExportTemplateFilter
|
||||||
|
|
||||||
|
|
||||||
class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
|
#
|
||||||
|
# Topology maps
|
||||||
|
#
|
||||||
|
|
||||||
|
class TopologyMapViewSet(ModelViewSet):
|
||||||
queryset = TopologyMap.objects.select_related('site')
|
queryset = TopologyMap.objects.select_related('site')
|
||||||
serializer_class = serializers.TopologyMapSerializer
|
serializer_class = serializers.TopologyMapSerializer
|
||||||
write_serializer_class = serializers.WritableTopologyMapSerializer
|
|
||||||
filter_class = filters.TopologyMapFilter
|
filter_class = filters.TopologyMapFilter
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def render(self, request, pk):
|
def render(self, request, pk):
|
||||||
|
|
||||||
tmap = get_object_or_404(TopologyMap, pk=pk)
|
tmap = get_object_or_404(TopologyMap, pk=pk)
|
||||||
@@ -70,7 +102,7 @@ class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = tmap.render(img_format=img_format)
|
data = tmap.render(img_format=img_format)
|
||||||
except:
|
except Exception:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
|
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
|
||||||
"installed correctly."
|
"installed correctly."
|
||||||
@@ -82,12 +114,127 @@ class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagViewSet(ModelViewSet):
|
||||||
|
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
|
||||||
|
serializer_class = serializers.TagSerializer
|
||||||
|
filter_class = filters.TagFilter
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Image attachments
|
||||||
|
#
|
||||||
|
|
||||||
|
class ImageAttachmentViewSet(ModelViewSet):
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
write_serializer_class = serializers.WritableImageAttachmentSerializer
|
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config contexts
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigContextViewSet(ModelViewSet):
|
||||||
|
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
||||||
|
serializer_class = serializers.ConfigContextSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Reports
|
||||||
|
#
|
||||||
|
|
||||||
|
class ReportViewSet(ViewSet):
|
||||||
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
_ignore_model_permissions = True
|
||||||
|
exclude_from_schema = True
|
||||||
|
lookup_value_regex = '[^/]+' # Allow dots
|
||||||
|
|
||||||
|
def _retrieve_report(self, pk):
|
||||||
|
|
||||||
|
# Read the PK as "<module>.<report>"
|
||||||
|
if '.' not in pk:
|
||||||
|
raise Http404
|
||||||
|
module_name, report_name = pk.split('.', 1)
|
||||||
|
|
||||||
|
# Raise a 404 on an invalid Report module/name
|
||||||
|
report = get_report(module_name, report_name)
|
||||||
|
if report is None:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
"""
|
||||||
|
Compile all reports and their related results (if any). Result data is deferred in the list view.
|
||||||
|
"""
|
||||||
|
report_list = []
|
||||||
|
|
||||||
|
# Iterate through all available Reports.
|
||||||
|
for module_name, reports in get_reports():
|
||||||
|
for report in reports:
|
||||||
|
|
||||||
|
# Attach the relevant ReportResult (if any) to each Report.
|
||||||
|
report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()
|
||||||
|
report_list.append(report)
|
||||||
|
|
||||||
|
serializer = serializers.ReportSerializer(report_list, many=True, context={
|
||||||
|
'request': request,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def retrieve(self, request, pk):
|
||||||
|
"""
|
||||||
|
Retrieve a single Report identified as "<module>.<report>".
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Retrieve the Report and ReportResult, if any.
|
||||||
|
report = self._retrieve_report(pk)
|
||||||
|
report.result = ReportResult.objects.filter(report=report.full_name).first()
|
||||||
|
|
||||||
|
serializer = serializers.ReportDetailSerializer(report)
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def run(self, request, pk):
|
||||||
|
"""
|
||||||
|
Run a Report and create a new ReportResult, overwriting any previous result for the Report.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check that the user has permission to run reports.
|
||||||
|
if not request.user.has_perm('extras.add_reportresult'):
|
||||||
|
raise PermissionDenied("This user does not have permission to run reports.")
|
||||||
|
|
||||||
|
# Retrieve and run the Report. This will create a new ReportResult.
|
||||||
|
report = self._retrieve_report(pk)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
serializer = serializers.ReportDetailSerializer(report)
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
Retrieve a list of recent changes.
|
||||||
|
"""
|
||||||
|
queryset = ObjectChange.objects.select_related('user')
|
||||||
|
serializer_class = serializers.ObjectChangeSerializer
|
||||||
|
filter_class = filters.ObjectChangeFilter
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# User activity
|
||||||
|
#
|
||||||
|
|
||||||
class RecentActivityViewSet(ReadOnlyModelViewSet):
|
class RecentActivityViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
List all UserActions to provide a log of recent activity.
|
List all UserActions to provide a log of recent activity.
|
||||||
|
|||||||
33
netbox/extras/apps.py
Normal file
33
netbox/extras/apps.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class ExtrasConfig(AppConfig):
|
||||||
|
name = "extras"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# Check that we can connect to the configured Redis database if webhooks are enabled.
|
||||||
|
if settings.WEBHOOKS_ENABLED:
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"WEBHOOKS_ENABLED is True but the redis Python package is not installed. (Try 'pip install "
|
||||||
|
"redis'.)"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
rs = redis.Redis(
|
||||||
|
host=settings.REDIS_HOST,
|
||||||
|
port=settings.REDIS_PORT,
|
||||||
|
db=settings.REDIS_DATABASE,
|
||||||
|
password=settings.REDIS_PASSWORD or None,
|
||||||
|
)
|
||||||
|
rs.ping()
|
||||||
|
except redis.exceptions.ConnectionError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
|
||||||
|
"configuration.py."
|
||||||
|
)
|
||||||
@@ -3,10 +3,12 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
# Models which support custom fields
|
# Models which support custom fields
|
||||||
CUSTOMFIELD_MODELS = (
|
CUSTOMFIELD_MODELS = (
|
||||||
'site', 'rack', 'devicetype', 'device', # DCIM
|
'provider', 'circuit', # Circuits
|
||||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
|
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||||
'provider', 'circuit', # Circuits
|
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||||
'tenant', # Tenants
|
'secret', # Secrets
|
||||||
|
'tenant', # Tenancy
|
||||||
|
'cluster', 'virtualmachine', # Virtualization
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom field types
|
# Custom field types
|
||||||
@@ -25,6 +27,16 @@ CUSTOMFIELD_TYPE_CHOICES = (
|
|||||||
(CF_TYPE_SELECT, 'Selection'),
|
(CF_TYPE_SELECT, 'Selection'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Custom field filter logic choices
|
||||||
|
CF_FILTER_DISABLED = 0
|
||||||
|
CF_FILTER_LOOSE = 1
|
||||||
|
CF_FILTER_EXACT = 2
|
||||||
|
CF_FILTER_CHOICES = (
|
||||||
|
(CF_FILTER_DISABLED, 'Disabled'),
|
||||||
|
(CF_FILTER_LOOSE, 'Loose'),
|
||||||
|
(CF_FILTER_EXACT, 'Exact'),
|
||||||
|
)
|
||||||
|
|
||||||
# Graph types
|
# Graph types
|
||||||
GRAPH_TYPE_INTERFACE = 100
|
GRAPH_TYPE_INTERFACE = 100
|
||||||
GRAPH_TYPE_PROVIDER = 200
|
GRAPH_TYPE_PROVIDER = 200
|
||||||
@@ -37,12 +49,35 @@ GRAPH_TYPE_CHOICES = (
|
|||||||
|
|
||||||
# Models which support export templates
|
# Models which support export templates
|
||||||
EXPORTTEMPLATE_MODELS = [
|
EXPORTTEMPLATE_MODELS = [
|
||||||
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
|
|
||||||
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
|
|
||||||
'provider', 'circuit', # Circuits
|
'provider', 'circuit', # Circuits
|
||||||
'tenant', # Tenants
|
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
|
||||||
|
'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', # DCIM
|
||||||
|
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||||
|
'secret', # Secrets
|
||||||
|
'tenant', # Tenancy
|
||||||
|
'cluster', 'virtualmachine', # Virtualization
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Topology map types
|
||||||
|
TOPOLOGYMAP_TYPE_NETWORK = 1
|
||||||
|
TOPOLOGYMAP_TYPE_CONSOLE = 2
|
||||||
|
TOPOLOGYMAP_TYPE_POWER = 3
|
||||||
|
TOPOLOGYMAP_TYPE_CHOICES = (
|
||||||
|
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
|
||||||
|
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
|
||||||
|
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change log actions
|
||||||
|
OBJECTCHANGE_ACTION_CREATE = 1
|
||||||
|
OBJECTCHANGE_ACTION_UPDATE = 2
|
||||||
|
OBJECTCHANGE_ACTION_DELETE = 3
|
||||||
|
OBJECTCHANGE_ACTION_CHOICES = (
|
||||||
|
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
|
||||||
|
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
|
||||||
|
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
|
||||||
|
)
|
||||||
|
|
||||||
# User action types
|
# User action types
|
||||||
ACTION_CREATE = 1
|
ACTION_CREATE = 1
|
||||||
ACTION_IMPORT = 2
|
ACTION_IMPORT = 2
|
||||||
@@ -60,3 +95,37 @@ ACTION_CHOICES = (
|
|||||||
(ACTION_DELETE, 'deleted'),
|
(ACTION_DELETE, 'deleted'),
|
||||||
(ACTION_BULK_DELETE, 'bulk deleted'),
|
(ACTION_BULK_DELETE, 'bulk deleted'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Report logging levels
|
||||||
|
LOG_DEFAULT = 0
|
||||||
|
LOG_SUCCESS = 10
|
||||||
|
LOG_INFO = 20
|
||||||
|
LOG_WARNING = 30
|
||||||
|
LOG_FAILURE = 40
|
||||||
|
LOG_LEVEL_CODES = {
|
||||||
|
LOG_DEFAULT: 'default',
|
||||||
|
LOG_SUCCESS: 'success',
|
||||||
|
LOG_INFO: 'info',
|
||||||
|
LOG_WARNING: 'warning',
|
||||||
|
LOG_FAILURE: 'failure',
|
||||||
|
}
|
||||||
|
|
||||||
|
# webhook content types
|
||||||
|
WEBHOOK_CT_JSON = 1
|
||||||
|
WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
|
||||||
|
WEBHOOK_CT_CHOICES = (
|
||||||
|
(WEBHOOK_CT_JSON, 'application/json'),
|
||||||
|
(WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Models which support registered webhooks
|
||||||
|
WEBHOOK_MODELS = (
|
||||||
|
'provider', 'circuit', # Circuits
|
||||||
|
'site', 'rack', 'devicetype', 'device', 'virtualchassis', # DCIM
|
||||||
|
'consoleport', 'consoleserverport', 'powerport', 'poweroutlet',
|
||||||
|
'interface', 'devicebay', 'inventoryitem',
|
||||||
|
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||||
|
'secret', # Secrets
|
||||||
|
'tenant', # Tenancy
|
||||||
|
'cluster', 'virtualmachine', # Virtualization
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
|
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
@@ -14,26 +16,47 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cf_type, *args, **kwargs):
|
def __init__(self, custom_field, *args, **kwargs):
|
||||||
self.cf_type = cf_type
|
self.cf_type = custom_field.type
|
||||||
|
self.filter_logic = custom_field.filter_logic
|
||||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def filter(self, queryset, value):
|
def filter(self, queryset, value):
|
||||||
|
|
||||||
# Skip filter on empty value
|
# Skip filter on empty value
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
# Treat 0 as None for Select fields
|
|
||||||
try:
|
# Selection fields get special treatment (values must be integers)
|
||||||
if self.cf_type == CF_TYPE_SELECT and int(value) == 0:
|
if self.cf_type == CF_TYPE_SELECT:
|
||||||
return queryset.exclude(
|
try:
|
||||||
custom_field_values__field__name=self.name,
|
# Treat 0 as None
|
||||||
)
|
if int(value) == 0:
|
||||||
except ValueError:
|
return queryset.exclude(
|
||||||
pass
|
custom_field_values__field__name=self.name,
|
||||||
return queryset.filter(
|
)
|
||||||
custom_field_values__field__name=self.name,
|
# Match on exact CustomFieldChoice PK
|
||||||
custom_field_values__serialized_value__icontains=value,
|
else:
|
||||||
)
|
return queryset.filter(
|
||||||
|
custom_field_values__field__name=self.name,
|
||||||
|
custom_field_values__serialized_value=value,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
# Apply the assigned filter logic (exact or loose)
|
||||||
|
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
custom_field_values__field__name=self.name,
|
||||||
|
custom_field_values__serialized_value=value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
custom_field_values__field__name=self.name,
|
||||||
|
custom_field_values__serialized_value__icontains=value
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||||
@@ -45,9 +68,9 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
|||||||
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
|
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
|
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, custom_field=cf)
|
||||||
|
|
||||||
|
|
||||||
class GraphFilter(django_filters.FilterSet):
|
class GraphFilter(django_filters.FilterSet):
|
||||||
@@ -64,6 +87,25 @@ class ExportTemplateFilter(django_filters.FilterSet):
|
|||||||
fields = ['content_type', 'name']
|
fields = ['content_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class TagFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TopologyMapFilter(django_filters.FilterSet):
|
class TopologyMapFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
@@ -82,6 +124,26 @@ class TopologyMapFilter(django_filters.FilterSet):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectChangeFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
time = django_filters.DateTimeFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectChange
|
||||||
|
fields = ['user', 'user_name', 'request_id', 'action', 'changed_object_type', 'object_repr']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(user_name__icontains=value) |
|
||||||
|
Q(object_repr__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserActionFilter(django_filters.FilterSet):
|
class UserActionFilter(django_filters.FilterSet):
|
||||||
username = django_filters.ModelMultipleChoiceFilter(
|
username = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='user__username',
|
name='user__username',
|
||||||
|
|||||||
@@ -1,32 +1,44 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from mptt.forms import TreeNodeMultipleChoiceField
|
||||||
|
from taggit.forms import TagField
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
from dcim.models import Region
|
||||||
from .models import (
|
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
|
||||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
|
from .constants import (
|
||||||
ImageAttachment,
|
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
|
OBJECTCHANGE_ACTION_CHOICES,
|
||||||
)
|
)
|
||||||
|
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
||||||
"""
|
"""
|
||||||
Retrieve all CustomFields applicable to the given ContentType
|
Retrieve all CustomFields applicable to the given ContentType
|
||||||
"""
|
"""
|
||||||
field_dict = OrderedDict()
|
field_dict = OrderedDict()
|
||||||
kwargs = {'obj_type': content_type}
|
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||||
if filterable_only:
|
if filterable_only:
|
||||||
kwargs['is_filterable'] = True
|
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
|
||||||
custom_fields = CustomField.objects.filter(**kwargs)
|
|
||||||
|
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
field_name = 'cf_{}'.format(str(cf.name))
|
field_name = 'cf_{}'.format(str(cf.name))
|
||||||
|
initial = cf.default if not bulk_edit else None
|
||||||
|
|
||||||
# Integer
|
# Integer
|
||||||
if cf.type == CF_TYPE_INTEGER:
|
if cf.type == CF_TYPE_INTEGER:
|
||||||
field = forms.IntegerField(required=cf.required, initial=cf.default)
|
field = forms.IntegerField(required=cf.required, initial=initial)
|
||||||
|
|
||||||
# Boolean
|
# Boolean
|
||||||
elif cf.type == CF_TYPE_BOOLEAN:
|
elif cf.type == CF_TYPE_BOOLEAN:
|
||||||
@@ -35,33 +47,41 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
(1, 'True'),
|
(1, 'True'),
|
||||||
(0, 'False'),
|
(0, 'False'),
|
||||||
)
|
)
|
||||||
if cf.default.lower() in ['true', 'yes', '1']:
|
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||||
initial = 1
|
initial = 1
|
||||||
elif cf.default.lower() in ['false', 'no', '0']:
|
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||||
initial = 0
|
initial = 0
|
||||||
else:
|
else:
|
||||||
initial = None
|
initial = None
|
||||||
field = forms.NullBooleanField(required=cf.required, initial=initial,
|
field = forms.NullBooleanField(
|
||||||
widget=forms.Select(choices=choices))
|
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
|
||||||
|
)
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
elif cf.type == CF_TYPE_DATE:
|
elif cf.type == CF_TYPE_DATE:
|
||||||
field = forms.DateField(required=cf.required, initial=cf.default, help_text="Date format: YYYY-MM-DD")
|
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
|
||||||
|
|
||||||
# Select
|
# Select
|
||||||
elif cf.type == CF_TYPE_SELECT:
|
elif cf.type == CF_TYPE_SELECT:
|
||||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||||
if not cf.required or bulk_edit or filterable_only:
|
if not cf.required or bulk_edit or filterable_only:
|
||||||
choices = [(None, '---------')] + choices
|
choices = [(None, '---------')] + choices
|
||||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
# Check for a default choice
|
||||||
|
default_choice = None
|
||||||
|
if initial:
|
||||||
|
try:
|
||||||
|
default_choice = cf.choices.get(value=initial).pk
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
|
||||||
|
|
||||||
# URL
|
# URL
|
||||||
elif cf.type == CF_TYPE_URL:
|
elif cf.type == CF_TYPE_URL:
|
||||||
field = LaxURLField(required=cf.required, initial=cf.default)
|
field = LaxURLField(required=cf.required, initial=initial)
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
else:
|
else:
|
||||||
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
|
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
|
||||||
|
|
||||||
field.model = cf
|
field.model = cf
|
||||||
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
||||||
@@ -162,8 +182,87 @@ class CustomFieldFilterForm(forms.Form):
|
|||||||
self.fields[name] = field
|
self.fields[name] = field
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class AddRemoveTagsForm(forms.Form):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AddRemoveTagsForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Add add/remove tags fields
|
||||||
|
self.fields['add_tags'] = TagField(required=False)
|
||||||
|
self.fields['remove_tags'] = TagField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config contexts
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
regions = TreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
data = JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigContext
|
||||||
|
fields = [
|
||||||
|
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Image attachments
|
||||||
|
#
|
||||||
|
|
||||||
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ['name', 'image']
|
fields = ['name', 'image']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = ObjectChange
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Search'
|
||||||
|
)
|
||||||
|
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
|
||||||
|
time_0 = forms.DateTimeField(
|
||||||
|
label='After',
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
time_1 = forms.DateTimeField(
|
||||||
|
label='Before',
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
action = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
user = forms.ModelChoiceField(
|
||||||
|
queryset=User.objects.order_by('username'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from django.conf import settings
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
|
||||||
|
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
|
|
||||||
|
|
||||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||||
### Python {python} | Django {django} | NetBox {netbox}
|
### Python {python} | Django {django} | NetBox {netbox}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from ncclient.transport.errors import AuthenticationError
|
|
||||||
from paramiko import AuthenticationException
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from ncclient.transport.errors import AuthenticationError
|
||||||
|
from paramiko import AuthenticationException
|
||||||
|
|
||||||
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
from dcim.models import DEVICE_STATUS_ACTIVE, Device, InventoryItem, Site
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -41,7 +41,7 @@ class Command(BaseCommand):
|
|||||||
self.password = getpass("Password: ")
|
self.password = getpass("Password: ")
|
||||||
|
|
||||||
# Attempt to inventory only active devices
|
# Attempt to inventory only active devices
|
||||||
device_list = Device.objects.filter(status=STATUS_ACTIVE)
|
device_list = Device.objects.filter(status=DEVICE_STATUS_ACTIVE)
|
||||||
|
|
||||||
# --site: Include only devices belonging to specified site(s)
|
# --site: Include only devices belonging to specified site(s)
|
||||||
if options['site']:
|
if options['site']:
|
||||||
|
|||||||
46
netbox/extras/management/commands/runreport.py
Normal file
46
netbox/extras/management/commands/runreport.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from extras.reports import get_reports
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Run a report to validate data in NetBox"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('reports', nargs='+', help="Report(s) to run")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
# Gather all available reports
|
||||||
|
reports = get_reports()
|
||||||
|
|
||||||
|
# Run reports
|
||||||
|
for module_name, report_list in reports:
|
||||||
|
for report in report_list:
|
||||||
|
if module_name in options['reports'] or report.full_name in options['reports']:
|
||||||
|
|
||||||
|
# Run the report and create a new ReportResult
|
||||||
|
self.stdout.write(
|
||||||
|
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
|
||||||
|
)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
# Report on success/failure
|
||||||
|
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
|
||||||
|
for test_name, attrs in report.result.data.items():
|
||||||
|
self.stdout.write(
|
||||||
|
"\t{}: {} success, {} info, {} warning, {} failure".format(
|
||||||
|
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wrap things up
|
||||||
|
self.stdout.write(
|
||||||
|
"[{:%H:%M:%S}] Finished".format(timezone.now())
|
||||||
|
)
|
||||||
71
netbox/extras/middleware.py
Normal file
71
netbox/extras/middleware.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models.signals import post_delete, post_save
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||||
|
from .models import ObjectChange
|
||||||
|
|
||||||
|
|
||||||
|
_thread_locals = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
def mark_object_changed(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Mark an object as having been created, saved, or updated. At the end of the request, this change will be recorded.
|
||||||
|
We have to wait until the *end* of the request to the serialize the object, because related fields like tags and
|
||||||
|
custom fields have not yet been updated when the post_save signal is emitted.
|
||||||
|
"""
|
||||||
|
if not hasattr(instance, 'log_change'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine what action is being performed. The post_save signal sends a `created` boolean, whereas post_delete
|
||||||
|
# does not.
|
||||||
|
if 'created' in kwargs:
|
||||||
|
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
|
||||||
|
else:
|
||||||
|
action = OBJECTCHANGE_ACTION_DELETE
|
||||||
|
|
||||||
|
_thread_locals.changed_objects.append((instance, action))
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeLoggingMiddleware(object):
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
|
||||||
|
# Initialize the list of changed objects
|
||||||
|
_thread_locals.changed_objects = []
|
||||||
|
|
||||||
|
# Assign a random unique ID to the request. This will be used to associate multiple object changes made during
|
||||||
|
# the same request.
|
||||||
|
request.id = uuid.uuid4()
|
||||||
|
|
||||||
|
# Connect mark_object_changed to the post_save and post_delete receivers
|
||||||
|
post_save.connect(mark_object_changed, dispatch_uid='record_object_saved')
|
||||||
|
post_delete.connect(mark_object_changed, dispatch_uid='record_object_deleted')
|
||||||
|
|
||||||
|
# Process the request
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Record object changes
|
||||||
|
for obj, action in _thread_locals.changed_objects:
|
||||||
|
if obj.pk:
|
||||||
|
obj.log_change(request.user, request.id, action)
|
||||||
|
|
||||||
|
# Housekeeping: 1% chance of clearing out expired ObjectChanges
|
||||||
|
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
||||||
|
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||||
|
purged_count, _ = ObjectChange.objects.filter(
|
||||||
|
time__lt=cutoff
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -4,14 +4,6 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
from extras.models import TopologyMap
|
|
||||||
|
|
||||||
|
|
||||||
def commas_to_semicolons(apps, schema_editor):
|
|
||||||
for tm in TopologyMap.objects.filter(device_patterns__contains=','):
|
|
||||||
tm.device_patterns = tm.device_patterns.replace(',', ';')
|
|
||||||
tm.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@@ -25,5 +17,4 @@ class Migration(migrations.Migration):
|
|||||||
name='device_patterns',
|
name='device_patterns',
|
||||||
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||||
),
|
),
|
||||||
migrations.RunPython(commas_to_semicolons),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='customfield',
|
model_name='customfield',
|
||||||
name='default',
|
name='default',
|
||||||
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100),
|
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='customfield',
|
model_name='customfield',
|
||||||
|
|||||||
54
netbox/extras/migrations/0008_reports.py
Normal file
54
netbox/extras/migrations/0008_reports.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import connection, migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db.utils import OperationalError
|
||||||
|
|
||||||
|
|
||||||
|
def verify_postgresql_version(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Verify that PostgreSQL is version 9.4 or higher.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT VERSION()")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
|
||||||
|
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
||||||
|
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||||
|
|
||||||
|
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
|
||||||
|
except OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('extras', '0007_unicode_literals'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(verify_postgresql_version),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReportResult',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('report', models.CharField(max_length=255, unique=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('failed', models.BooleanField()),
|
||||||
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['report'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
20
netbox/extras/migrations/0009_topologymap_type.py
Normal file
20
netbox/extras/migrations/0009_topologymap_type.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-15 16:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0008_reports'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='topologymap',
|
||||||
|
name='type',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||||
|
|
||||||
|
|
||||||
|
def is_filterable_to_filter_logic(apps, schema_editor):
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
|
||||||
|
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
|
||||||
|
# Select fields match on primary key only
|
||||||
|
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_logic_to_is_filterable(apps, schema_editor):
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
|
||||||
|
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0009_topologymap_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='filter_logic',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='required',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='weight',
|
||||||
|
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(is_filterable_to_filter_logic, filter_logic_to_is_filterable),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='is_filterable',
|
||||||
|
),
|
||||||
|
]
|
||||||
29
netbox/extras/migrations/0011_django2.py
Normal file
29
netbox/extras/migrations/0011_django2.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 2.0.3 on 2018-03-30 14:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0010_customfield_filter_logic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='obj_type',
|
||||||
|
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfieldchoice',
|
||||||
|
name='field',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'type': 600}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='content_type',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
]
|
||||||
36
netbox/extras/migrations/0012_webhooks.py
Normal file
36
netbox/extras/migrations/0012_webhooks.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-05-30 17:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0011_django2'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Webhook',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=150, unique=True)),
|
||||||
|
('type_create', models.BooleanField(default=False, help_text='Call this webhook when a matching object is created.')),
|
||||||
|
('type_update', models.BooleanField(default=False, help_text='Call this webhook when a matching object is updated.')),
|
||||||
|
('type_delete', models.BooleanField(default=False, help_text='Call this webhook when a matching object is deleted.')),
|
||||||
|
('payload_url', models.CharField(help_text='A POST will be sent to this URL when the webhook is called.', max_length=500, verbose_name='URL')),
|
||||||
|
('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1, verbose_name='HTTP content type')),
|
||||||
|
('secret', models.CharField(blank=True, help_text="When provided, the request will include a 'X-Hook-Signature' header containing a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
('ssl_verification', models.BooleanField(default=True, help_text='Enable SSL certificate verification. Disable with caution!', verbose_name='SSL verification')),
|
||||||
|
('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='webhook',
|
||||||
|
unique_together=set([('payload_url', 'type_create', 'type_update', 'type_delete')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
40
netbox/extras/migrations/0013_objectchange.py
Normal file
40
netbox/extras/migrations/0013_objectchange.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-06-22 18:13
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0012_webhooks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ObjectChange',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('time', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user_name', models.CharField(editable=False, max_length=150)),
|
||||||
|
('request_id', models.UUIDField(editable=False)),
|
||||||
|
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
|
||||||
|
('changed_object_id', models.PositiveIntegerField()),
|
||||||
|
('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||||
|
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
|
||||||
|
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||||
|
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-time'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
45
netbox/extras/migrations/0014_configcontexts.py
Normal file
45
netbox/extras/migrations/0014_configcontexts.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 2.0.6 on 2018-06-29 13:34
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0005_change_logging'),
|
||||||
|
('dcim', '0060_change_logging'),
|
||||||
|
('extras', '0013_objectchange'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConfigContext',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=100)),
|
||||||
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
|
('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
|
||||||
|
('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
|
||||||
|
('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
|
||||||
|
('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
|
||||||
|
('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['weight', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='obj_type',
|
||||||
|
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='obj_type',
|
||||||
|
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'rackgroup', 'device', 'interface', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vlangroup', 'vrf', 'service', 'tenant', 'tenantgroup', 'cluster', 'clustergroup', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,21 +1,104 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import graphviz
|
|
||||||
|
|
||||||
|
import graphviz
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||||
from utilities.utils import foreground_color
|
from utilities.utils import foreground_color
|
||||||
from .constants import *
|
from .constants import *
|
||||||
|
from .querysets import ConfigContextQuerySet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Webhook(models.Model):
|
||||||
|
"""
|
||||||
|
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||||
|
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
||||||
|
Each Webhook can be limited to firing only on certain actions or certain object types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj_type = models.ManyToManyField(
|
||||||
|
to=ContentType,
|
||||||
|
related_name='webhooks',
|
||||||
|
verbose_name='Object types',
|
||||||
|
limit_choices_to={'model__in': WEBHOOK_MODELS},
|
||||||
|
help_text="The object(s) to which this Webhook applies."
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=150,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
type_create = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Call this webhook when a matching object is created."
|
||||||
|
)
|
||||||
|
type_update = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Call this webhook when a matching object is updated."
|
||||||
|
)
|
||||||
|
type_delete = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Call this webhook when a matching object is deleted."
|
||||||
|
)
|
||||||
|
payload_url = models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
verbose_name='URL',
|
||||||
|
help_text="A POST will be sent to this URL when the webhook is called."
|
||||||
|
)
|
||||||
|
http_content_type = models.PositiveSmallIntegerField(
|
||||||
|
choices=WEBHOOK_CT_CHOICES,
|
||||||
|
default=WEBHOOK_CT_JSON,
|
||||||
|
verbose_name='HTTP content type'
|
||||||
|
)
|
||||||
|
secret = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
help_text="When provided, the request will include a 'X-Hook-Signature' "
|
||||||
|
"header containing a HMAC hex digest of the payload body using "
|
||||||
|
"the secret as the key. The secret is not transmitted in "
|
||||||
|
"the request."
|
||||||
|
)
|
||||||
|
enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
ssl_verification = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='SSL verification',
|
||||||
|
help_text="Enable SSL certificate verification. Disable with caution!"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""
|
||||||
|
Validate model
|
||||||
|
"""
|
||||||
|
if not self.type_create and not self.type_delete and not self.type_update:
|
||||||
|
raise ValidationError(
|
||||||
|
"You must select at least one type: create, update, and/or delete."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -52,22 +135,51 @@ class CustomFieldModel(object):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomField(models.Model):
|
class CustomField(models.Model):
|
||||||
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
obj_type = models.ManyToManyField(
|
||||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
to=ContentType,
|
||||||
help_text="The object(s) to which this field applies.")
|
related_name='custom_fields',
|
||||||
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
|
verbose_name='Object(s)',
|
||||||
name = models.CharField(max_length=50, unique=True)
|
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||||
label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
|
help_text='The object(s) to which this field applies.'
|
||||||
"provided, the field's name will be used)")
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
type = models.PositiveSmallIntegerField(
|
||||||
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
|
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||||
"new objects or editing an existing object.")
|
default=CF_TYPE_TEXT
|
||||||
is_filterable = models.BooleanField(default=True, help_text="This field can be used to filter objects.")
|
)
|
||||||
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. Use \"true\" or "
|
name = models.CharField(
|
||||||
"\"false\" for booleans. N/A for selection "
|
max_length=50,
|
||||||
"fields.")
|
unique=True
|
||||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Fields with higher weights appear lower in a "
|
)
|
||||||
"form")
|
label = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
help_text='Name of the field as displayed to users (if not provided, '
|
||||||
|
'the field\'s name will be used)'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
required = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='If true, this field is required when creating new objects '
|
||||||
|
'or editing an existing object.'
|
||||||
|
)
|
||||||
|
filter_logic = models.PositiveSmallIntegerField(
|
||||||
|
choices=CF_FILTER_CHOICES,
|
||||||
|
default=CF_FILTER_LOOSE,
|
||||||
|
help_text='Loose matches any instance of a given string; exact '
|
||||||
|
'matches the entire field.'
|
||||||
|
)
|
||||||
|
default = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
help_text='Default value for the field. Use "true" or "false" for booleans.'
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100,
|
||||||
|
help_text='Fields with higher weights appear lower in a form.'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'name']
|
ordering = ['weight', 'name']
|
||||||
@@ -98,7 +210,7 @@ class CustomField(models.Model):
|
|||||||
"""
|
"""
|
||||||
Convert a string into the object it represents depending on the type of field
|
Convert a string into the object it represents depending on the type of field
|
||||||
"""
|
"""
|
||||||
if serialized_value is '':
|
if serialized_value == '':
|
||||||
return None
|
return None
|
||||||
if self.type == CF_TYPE_INTEGER:
|
if self.type == CF_TYPE_INTEGER:
|
||||||
return int(serialized_value)
|
return int(serialized_value)
|
||||||
@@ -114,11 +226,24 @@ class CustomField(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomFieldValue(models.Model):
|
class CustomFieldValue(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
|
field = models.ForeignKey(
|
||||||
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
|
to='extras.CustomField',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='values'
|
||||||
|
)
|
||||||
|
obj_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
obj_id = models.PositiveIntegerField()
|
obj_id = models.PositiveIntegerField()
|
||||||
obj = GenericForeignKey('obj_type', 'obj_id')
|
obj = GenericForeignKey(
|
||||||
serialized_value = models.CharField(max_length=255)
|
ct_field='obj_type',
|
||||||
|
fk_field='obj_id'
|
||||||
|
)
|
||||||
|
serialized_value = models.CharField(
|
||||||
|
max_length=255
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['obj_type', 'obj_id']
|
ordering = ['obj_type', 'obj_id']
|
||||||
@@ -145,10 +270,19 @@ class CustomFieldValue(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomFieldChoice(models.Model):
|
class CustomFieldChoice(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
|
field = models.ForeignKey(
|
||||||
on_delete=models.CASCADE)
|
to='extras.CustomField',
|
||||||
value = models.CharField(max_length=100)
|
on_delete=models.CASCADE,
|
||||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Higher weights appear lower in the list")
|
related_name='choices',
|
||||||
|
limit_choices_to={'type': CF_TYPE_SELECT}
|
||||||
|
)
|
||||||
|
value = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100,
|
||||||
|
help_text='Higher weights appear lower in the list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['field', 'weight', 'value']
|
ordering = ['field', 'weight', 'value']
|
||||||
@@ -174,11 +308,24 @@ class CustomFieldChoice(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Graph(models.Model):
|
class Graph(models.Model):
|
||||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
type = models.PositiveSmallIntegerField(
|
||||||
weight = models.PositiveSmallIntegerField(default=1000)
|
choices=GRAPH_TYPE_CHOICES
|
||||||
name = models.CharField(max_length=100, verbose_name='Name')
|
)
|
||||||
source = models.CharField(max_length=500, verbose_name='Source URL')
|
weight = models.PositiveSmallIntegerField(
|
||||||
link = models.URLField(verbose_name='Link URL', blank=True)
|
default=1000
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name='Name'
|
||||||
|
)
|
||||||
|
source = models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
verbose_name='Source URL'
|
||||||
|
)
|
||||||
|
link = models.URLField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Link URL'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['type', 'weight', 'name']
|
ordering = ['type', 'weight', 'name']
|
||||||
@@ -204,13 +351,26 @@ class Graph(models.Model):
|
|||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ExportTemplate(models.Model):
|
class ExportTemplate(models.Model):
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
description = models.CharField(max_length=200, blank=True)
|
|
||||||
template_code = models.TextField()
|
template_code = models.TextField()
|
||||||
mime_type = models.CharField(max_length=15, blank=True)
|
mime_type = models.CharField(
|
||||||
file_extension = models.CharField(max_length=15, blank=True)
|
max_length=15,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
file_extension = models.CharField(
|
||||||
|
max_length=15,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['content_type', 'name']
|
ordering = ['content_type', 'name']
|
||||||
@@ -221,19 +381,25 @@ class ExportTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{}: {}'.format(self.content_type, self.name)
|
return '{}: {}'.format(self.content_type, self.name)
|
||||||
|
|
||||||
def to_response(self, context_dict, filename):
|
def render_to_response(self, queryset):
|
||||||
"""
|
"""
|
||||||
Render the template to an HTTP response, delivered as a named file attachment
|
Render the template to an HTTP response, delivered as a named file attachment
|
||||||
"""
|
"""
|
||||||
template = Template(self.template_code)
|
template = Template(self.template_code)
|
||||||
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
||||||
output = template.render(Context(context_dict))
|
output = template.render(Context({'queryset': queryset}))
|
||||||
|
|
||||||
# Replace CRLF-style line terminators
|
# Replace CRLF-style line terminators
|
||||||
output = output.replace('\r\n', '\n')
|
output = output.replace('\r\n', '\n')
|
||||||
|
|
||||||
|
# Build the response
|
||||||
response = HttpResponse(output, content_type=mime_type)
|
response = HttpResponse(output, content_type=mime_type)
|
||||||
if self.file_extension:
|
filename = 'netbox_{}{}'.format(
|
||||||
filename += '.{}'.format(self.file_extension)
|
queryset.model._meta.verbose_name_plural,
|
||||||
|
'.{}'.format(self.file_extension) if self.file_extension else ''
|
||||||
|
)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -243,15 +409,35 @@ class ExportTemplate(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class TopologyMap(models.Model):
|
class TopologyMap(models.Model):
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(
|
||||||
slug = models.SlugField(unique=True)
|
max_length=50,
|
||||||
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
|
unique=True
|
||||||
device_patterns = models.TextField(
|
)
|
||||||
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
|
slug = models.SlugField(
|
||||||
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
|
unique=True
|
||||||
"Devices will be rendered in the order they are defined."
|
)
|
||||||
|
type = models.PositiveSmallIntegerField(
|
||||||
|
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
||||||
|
default=TOPOLOGYMAP_TYPE_NETWORK
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='topology_maps',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
device_patterns = models.TextField(
|
||||||
|
help_text='Identify devices to include in the diagram using regular '
|
||||||
|
'expressions, one per line. Each line will result in a new '
|
||||||
|
'tier of the drawing. Separate multiple regexes within a '
|
||||||
|
'line using semicolons. Devices will be rendered in the '
|
||||||
|
'order they are defined.'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -267,26 +453,34 @@ class TopologyMap(models.Model):
|
|||||||
|
|
||||||
def render(self, img_format='png'):
|
def render(self, img_format='png'):
|
||||||
|
|
||||||
from circuits.models import CircuitTermination
|
from dcim.models import Device
|
||||||
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
|
|
||||||
|
|
||||||
# Construct the graph
|
# Construct the graph
|
||||||
graph = graphviz.Graph()
|
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||||
graph.graph_attr['ranksep'] = '1'
|
G = graphviz.Graph
|
||||||
|
else:
|
||||||
|
G = graphviz.Digraph
|
||||||
|
self.graph = G()
|
||||||
|
self.graph.graph_attr['ranksep'] = '1'
|
||||||
|
seen = set()
|
||||||
for i, device_set in enumerate(self.device_sets):
|
for i, device_set in enumerate(self.device_sets):
|
||||||
|
|
||||||
subgraph = graphviz.Graph(name='sg{}'.format(i))
|
subgraph = G(name='sg{}'.format(i))
|
||||||
subgraph.graph_attr['rank'] = 'same'
|
subgraph.graph_attr['rank'] = 'same'
|
||||||
|
subgraph.graph_attr['directed'] = 'true'
|
||||||
|
|
||||||
# Add a pseudonode for each device_set to enforce hierarchical layout
|
# Add a pseudonode for each device_set to enforce hierarchical layout
|
||||||
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
|
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
|
||||||
if i:
|
if i:
|
||||||
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
||||||
|
|
||||||
# Add each device to the graph
|
# Add each device to the graph
|
||||||
devices = []
|
devices = []
|
||||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
||||||
|
# Remove duplicate devices
|
||||||
|
devices = [d for d in devices if d.id not in seen]
|
||||||
|
seen.update([d.id for d in devices])
|
||||||
for d in devices:
|
for d in devices:
|
||||||
bg_color = '#{}'.format(d.device_role.color)
|
bg_color = '#{}'.format(d.device_role.color)
|
||||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||||
@@ -296,31 +490,64 @@ class TopologyMap(models.Model):
|
|||||||
for j in range(0, len(devices) - 1):
|
for j in range(0, len(devices) - 1):
|
||||||
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
||||||
|
|
||||||
graph.subgraph(subgraph)
|
self.graph.subgraph(subgraph)
|
||||||
|
|
||||||
# Compile list of all devices
|
# Compile list of all devices
|
||||||
device_superset = Q()
|
device_superset = Q()
|
||||||
for device_set in self.device_sets:
|
for device_set in self.device_sets:
|
||||||
for query in device_set.split(';'): # Split regexes on semicolons
|
for query in device_set.split(';'): # Split regexes on semicolons
|
||||||
device_superset = device_superset | Q(name__regex=query)
|
device_superset = device_superset | Q(name__regex=query)
|
||||||
|
devices = Device.objects.filter(*(device_superset,))
|
||||||
|
|
||||||
|
# Draw edges depending on graph type
|
||||||
|
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||||
|
self.add_network_connections(devices)
|
||||||
|
elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
|
||||||
|
self.add_console_connections(devices)
|
||||||
|
elif self.type == TOPOLOGYMAP_TYPE_POWER:
|
||||||
|
self.add_power_connections(devices)
|
||||||
|
|
||||||
|
return self.graph.pipe(format=img_format)
|
||||||
|
|
||||||
|
def add_network_connections(self, devices):
|
||||||
|
|
||||||
|
from circuits.models import CircuitTermination
|
||||||
|
from dcim.models import InterfaceConnection
|
||||||
|
|
||||||
# Add all interface connections to the graph
|
# Add all interface connections to the graph
|
||||||
devices = Device.objects.filter(*(device_superset,))
|
|
||||||
connections = InterfaceConnection.objects.filter(
|
connections = InterfaceConnection.objects.filter(
|
||||||
interface_a__device__in=devices, interface_b__device__in=devices
|
interface_a__device__in=devices, interface_b__device__in=devices
|
||||||
)
|
)
|
||||||
for c in connections:
|
for c in connections:
|
||||||
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||||
|
|
||||||
# Add all circuits to the graph
|
# Add all circuits to the graph
|
||||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||||
peer_termination = termination.get_peer_termination()
|
peer_termination = termination.get_peer_termination()
|
||||||
if (peer_termination is not None and peer_termination.interface is not None and
|
if (peer_termination is not None and peer_termination.interface is not None and
|
||||||
peer_termination.interface.device in devices):
|
peer_termination.interface.device in devices):
|
||||||
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||||
|
|
||||||
return graph.pipe(format=img_format)
|
def add_console_connections(self, devices):
|
||||||
|
|
||||||
|
from dcim.models import ConsolePort
|
||||||
|
|
||||||
|
# Add all console connections to the graph
|
||||||
|
console_ports = ConsolePort.objects.filter(device__in=devices, cs_port__device__in=devices)
|
||||||
|
for cp in console_ports:
|
||||||
|
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||||
|
self.graph.edge(cp.cs_port.device.name, cp.device.name, style=style)
|
||||||
|
|
||||||
|
def add_power_connections(self, devices):
|
||||||
|
|
||||||
|
from dcim.models import PowerPort
|
||||||
|
|
||||||
|
# Add all power connections to the graph
|
||||||
|
power_ports = PowerPort.objects.filter(device__in=devices, power_outlet__device__in=devices)
|
||||||
|
for pp in power_ports:
|
||||||
|
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||||
|
self.graph.edge(pp.power_outlet.device.name, pp.device.name, style=style)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -346,14 +573,29 @@ class ImageAttachment(models.Model):
|
|||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
parent = GenericForeignKey('content_type', 'object_id')
|
parent = GenericForeignKey(
|
||||||
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
|
ct_field='content_type',
|
||||||
|
fk_field='object_id'
|
||||||
|
)
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to=image_upload,
|
||||||
|
height_field='image_height',
|
||||||
|
width_field='image_width'
|
||||||
|
)
|
||||||
image_height = models.PositiveSmallIntegerField()
|
image_height = models.PositiveSmallIntegerField()
|
||||||
image_width = models.PositiveSmallIntegerField()
|
image_width = models.PositiveSmallIntegerField()
|
||||||
name = models.CharField(max_length=50, blank=True)
|
name = models.CharField(
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
max_length=50,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -388,6 +630,225 @@ class ImageAttachment(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config contexts
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigContext(models.Model):
|
||||||
|
"""
|
||||||
|
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||||
|
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||||
|
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=1000
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
regions = models.ManyToManyField(
|
||||||
|
to='dcim.Region',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
sites = models.ManyToManyField(
|
||||||
|
to='dcim.Site',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
roles = models.ManyToManyField(
|
||||||
|
to='dcim.DeviceRole',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
platforms = models.ManyToManyField(
|
||||||
|
to='dcim.Platform',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
tenants = models.ManyToManyField(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
related_name='+',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
data = JSONField()
|
||||||
|
|
||||||
|
objects = ConfigContextQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['weight', 'name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextModel(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def get_config_context(self):
|
||||||
|
"""
|
||||||
|
Return the rendered configuration context for a device or VM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||||
|
data = OrderedDict()
|
||||||
|
for context in ConfigContext.objects.get_for_object(self):
|
||||||
|
data.update(context.data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Report results
|
||||||
|
#
|
||||||
|
|
||||||
|
class ReportResult(models.Model):
|
||||||
|
"""
|
||||||
|
This model stores the results from running a user-defined report.
|
||||||
|
"""
|
||||||
|
report = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
failed = models.BooleanField()
|
||||||
|
data = JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['report']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class ObjectChange(models.Model):
|
||||||
|
"""
|
||||||
|
Record a change to an object and the user account associated with that change. A change record may optionally
|
||||||
|
indicate an object related to the one being changed. For example, a change to an interface may also indicate the
|
||||||
|
parent device. This will ensure changes made to component models appear in the parent model's changelog.
|
||||||
|
"""
|
||||||
|
time = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='changes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
user_name = models.CharField(
|
||||||
|
max_length=150,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
request_id = models.UUIDField(
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
action = models.PositiveSmallIntegerField(
|
||||||
|
choices=OBJECTCHANGE_ACTION_CHOICES
|
||||||
|
)
|
||||||
|
changed_object_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
changed_object_id = models.PositiveIntegerField()
|
||||||
|
changed_object = GenericForeignKey(
|
||||||
|
ct_field='changed_object_type',
|
||||||
|
fk_field='changed_object_id'
|
||||||
|
)
|
||||||
|
related_object_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
related_object_id = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
related_object = GenericForeignKey(
|
||||||
|
ct_field='related_object_type',
|
||||||
|
fk_field='related_object_id'
|
||||||
|
)
|
||||||
|
object_repr = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
object_data = JSONField(
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = 'extras.api.serializers.ObjectChangeSerializer'
|
||||||
|
csv_headers = [
|
||||||
|
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||||
|
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-time']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} {} {} by {}'.format(
|
||||||
|
self.changed_object_type,
|
||||||
|
self.object_repr,
|
||||||
|
self.get_action_display().lower(),
|
||||||
|
self.user_name
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Record the user's name and the object's representation as static strings
|
||||||
|
self.user_name = self.user.username
|
||||||
|
self.object_repr = str(self.changed_object)
|
||||||
|
|
||||||
|
return super(ObjectChange, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:objectchange', args=[self.pk])
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.time,
|
||||||
|
self.user,
|
||||||
|
self.user_name,
|
||||||
|
self.request_id,
|
||||||
|
self.get_action_display(),
|
||||||
|
self.changed_object_type,
|
||||||
|
self.changed_object_id,
|
||||||
|
self.related_object_type,
|
||||||
|
self.related_object_id,
|
||||||
|
self.object_repr,
|
||||||
|
self.object_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# User actions
|
# User actions
|
||||||
#
|
#
|
||||||
@@ -440,12 +901,29 @@ class UserAction(models.Model):
|
|||||||
"""
|
"""
|
||||||
A record of an action (add, edit, or delete) performed on an object by a User.
|
A record of an action (add, edit, or delete) performed on an object by a User.
|
||||||
"""
|
"""
|
||||||
time = models.DateTimeField(auto_now_add=True, editable=False)
|
time = models.DateTimeField(
|
||||||
user = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
|
auto_now_add=True,
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
editable=False
|
||||||
object_id = models.PositiveIntegerField(blank=True, null=True)
|
)
|
||||||
action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES)
|
user = models.ForeignKey(
|
||||||
message = models.TextField(blank=True)
|
to=User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='actions'
|
||||||
|
)
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
object_id = models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
action = models.PositiveSmallIntegerField(
|
||||||
|
choices=ACTION_CHOICES
|
||||||
|
)
|
||||||
|
message = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
objects = UserActionManager()
|
objects = UserActionManager()
|
||||||
|
|
||||||
|
|||||||
23
netbox/extras/querysets.py
Normal file
23
netbox/extras/querysets.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db.models import Q, QuerySet
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextQuerySet(QuerySet):
|
||||||
|
|
||||||
|
def get_for_object(self, obj):
|
||||||
|
"""
|
||||||
|
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# `device_role` for Device; `role` for VirtualMachine
|
||||||
|
role = getattr(obj, 'device_role', None) or obj.role
|
||||||
|
|
||||||
|
return self.filter(
|
||||||
|
Q(regions=getattr(obj.site, 'region', None)) | Q(regions=None),
|
||||||
|
Q(sites=obj.site) | Q(sites=None),
|
||||||
|
Q(roles=role) | Q(roles=None),
|
||||||
|
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||||
|
Q(platforms=obj.platform) | Q(platforms=None),
|
||||||
|
is_active=True,
|
||||||
|
).order_by('weight', 'name')
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user