mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
2243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c532a6ad | ||
|
|
035dd16b36 | ||
|
|
48e8785725 | ||
|
|
b90d3306c5 | ||
|
|
7f7d634c38 | ||
|
|
45b13abed3 | ||
|
|
72cd7a3be2 | ||
|
|
3ccd54680e | ||
|
|
071d14c639 | ||
|
|
823e879432 | ||
|
|
739932b005 | ||
|
|
24f144fdc3 | ||
|
|
967800391e | ||
|
|
3ccb6637d7 | ||
|
|
8dfdefd428 | ||
|
|
ab2c58b626 | ||
|
|
ee4f5a8194 | ||
|
|
084a76b215 | ||
|
|
2398e9acbd | ||
|
|
5ad8b3cc70 | ||
|
|
7d14e1f248 | ||
|
|
819f8f469d | ||
|
|
a31b7a8800 | ||
|
|
24bdaecab4 | ||
|
|
8b3b517bab | ||
|
|
7fc2ef00ee | ||
|
|
cbd6c3ee69 | ||
|
|
3835adafb8 | ||
|
|
bbaa35c773 | ||
|
|
0fa8287811 | ||
|
|
78f4e5a89a | ||
|
|
3193c5549d | ||
|
|
ed71e7d2d9 | ||
|
|
33c299566a | ||
|
|
84634eb8c0 | ||
|
|
a4ff2181c5 | ||
|
|
fffa0def9e | ||
|
|
d0ede246e7 | ||
|
|
b8b78ffeb2 | ||
|
|
d2d10b59ac | ||
|
|
cb8e59edf2 | ||
|
|
1c0d783eec | ||
|
|
4fa72400be | ||
|
|
37c0062fae | ||
|
|
8504c3d2fd | ||
|
|
2d10e13057 | ||
|
|
1d7ba3e204 | ||
|
|
d966e22cf9 | ||
|
|
6a88fd2d60 | ||
|
|
b63999f385 | ||
|
|
82d06351e7 | ||
|
|
aa8bece724 | ||
|
|
241bdff7c8 | ||
|
|
168335a381 | ||
|
|
121726b731 | ||
|
|
0b812a03c6 | ||
|
|
79ae6efafb | ||
|
|
d2f108eeec | ||
|
|
c48beb10af | ||
|
|
ea9264ec49 | ||
|
|
5b65ed8a19 | ||
|
|
951ac252fe | ||
|
|
24c4e1df50 | ||
|
|
d247e49b70 | ||
|
|
a4c843ff13 | ||
|
|
ec6103448e | ||
|
|
0ca14463cd | ||
|
|
df80e8047a | ||
|
|
09fc2776df | ||
|
|
e4c2affb5f | ||
|
|
6fae4d5dee | ||
|
|
0c80e3e815 | ||
|
|
d32f070b5c | ||
|
|
b959f1f68b | ||
|
|
e1cab35db0 | ||
|
|
8014cc48b6 | ||
|
|
829f57e2d7 | ||
|
|
5f0b4a4b63 | ||
|
|
c5af4d47eb | ||
|
|
c37bfb682a | ||
|
|
3aaea6cc31 | ||
|
|
f85e5b6f75 | ||
|
|
d598571dc1 | ||
|
|
e873be95d5 | ||
|
|
dbaa4ab502 | ||
|
|
ac1e319cd9 | ||
|
|
a39424ac09 | ||
|
|
75319b99ae | ||
|
|
7f4f67aa8d | ||
|
|
fe1862120f | ||
|
|
759760e7d9 | ||
|
|
15b74da57c | ||
|
|
6f29cf5ddd | ||
|
|
0bba840e4d | ||
|
|
2156e0f51a | ||
|
|
2fc9c04228 | ||
|
|
d8e614c54d | ||
|
|
2ab26e25cc | ||
|
|
f195b309d4 | ||
|
|
5e41c5cadc | ||
|
|
d92d09bd80 | ||
|
|
227804b7ab | ||
|
|
eeae989c06 | ||
|
|
c67a9eb845 | ||
|
|
35ed881139 | ||
|
|
a58db9826e | ||
|
|
5250a223c3 | ||
|
|
15427fb743 | ||
|
|
7874a1539c | ||
|
|
9f23520712 | ||
|
|
63f1ec839c | ||
|
|
7e087ad5c6 | ||
|
|
93895b6b92 | ||
|
|
4d7fbcc49b | ||
|
|
3822d51888 | ||
|
|
aa5187dc39 | ||
|
|
e9be4f51e5 | ||
|
|
496403bde0 | ||
|
|
4f522c1cd1 | ||
|
|
5c3846a886 | ||
|
|
42f297e6c4 | ||
|
|
5d88c7b779 | ||
|
|
74047d19d0 | ||
|
|
8b63961d4d | ||
|
|
9c2470b67c | ||
|
|
240faee2b4 | ||
|
|
3d3d9b565a | ||
|
|
2e8f5919a8 | ||
|
|
faf6a7b623 | ||
|
|
8e4ab9f3a9 | ||
|
|
e4823a21e3 | ||
|
|
634af19945 | ||
|
|
b2ae0e8f44 | ||
|
|
4a3e5e9b22 | ||
|
|
43b4b7c55e | ||
|
|
b19a0e1cdf | ||
|
|
b92ccdd05e | ||
|
|
34cc6abec5 | ||
|
|
f59f03adfd | ||
|
|
0f2b0b146d | ||
|
|
e085fee101 | ||
|
|
fe03011177 | ||
|
|
9ffb079c8f | ||
|
|
050b6e6d88 | ||
|
|
422990a992 | ||
|
|
06ff7f75ce | ||
|
|
0b82b97c66 | ||
|
|
fcbe94de37 | ||
|
|
ce35a8e2ac | ||
|
|
bebd71895c | ||
|
|
ea18727bdb | ||
|
|
b90c5d1e43 | ||
|
|
0be31a8078 | ||
|
|
9162f4a226 | ||
|
|
3bf7d061c9 | ||
|
|
51e15b583f | ||
|
|
60427b9908 | ||
|
|
60bc42f630 | ||
|
|
63924a2ef4 | ||
|
|
2842153e21 | ||
|
|
3643ee1f89 | ||
|
|
14510b08a4 | ||
|
|
f2251bbb32 | ||
|
|
580f1f2a82 | ||
|
|
3c6c05f83e | ||
|
|
4e0f81e447 | ||
|
|
629ff39026 | ||
|
|
51d7e51119 | ||
|
|
3c32048aa8 | ||
|
|
166a3fff5c | ||
|
|
476a00d270 | ||
|
|
2e51a28deb | ||
|
|
0bd33ec823 | ||
|
|
813e10ed11 | ||
|
|
8b4d7c0dd7 | ||
|
|
395dc2e505 | ||
|
|
2406a7590b | ||
|
|
f86322fa15 | ||
|
|
88c10e851b | ||
|
|
c2abd58dcb | ||
|
|
1fa3e98eb0 | ||
|
|
42af939501 | ||
|
|
2def25cae6 | ||
|
|
d3fa3d63e7 | ||
|
|
5cc43f6907 | ||
|
|
d852f0182b | ||
|
|
9aed73d644 | ||
|
|
d1a2fe145d | ||
|
|
83f83d9b07 | ||
|
|
c4afd04cbc | ||
|
|
40d2934de0 | ||
|
|
e734fa3b0d | ||
|
|
50b0e024e5 | ||
|
|
f7d1a8b35f | ||
|
|
cd9000e7e9 | ||
|
|
af2db2d870 | ||
|
|
2559899b97 | ||
|
|
86cf7bd6b5 | ||
|
|
150855183a | ||
|
|
0aa4755565 | ||
|
|
7488d8834e | ||
|
|
022f6625b1 | ||
|
|
115c7c4d50 | ||
|
|
094e862abf | ||
|
|
55436ddff1 | ||
|
|
df1c85494b | ||
|
|
8e09151a02 | ||
|
|
18841d6c21 | ||
|
|
3fe5dadebc | ||
|
|
d3e77b09ef | ||
|
|
25ebb5c442 | ||
|
|
84c4e69062 | ||
|
|
ed7f7392db | ||
|
|
457886cb6c | ||
|
|
f724f46e00 | ||
|
|
6a8d75f00e | ||
|
|
65619fabc5 | ||
|
|
370534e28f | ||
|
|
cefdcb242c | ||
|
|
31ba73e2fa | ||
|
|
86c0a2d28b | ||
|
|
4778ac1392 | ||
|
|
58189df280 | ||
|
|
bc230e0ea5 | ||
|
|
21328382fc | ||
|
|
eca5966257 | ||
|
|
42cb54dd2a | ||
|
|
d07b427037 | ||
|
|
efcf31b2a9 | ||
|
|
e123599735 | ||
|
|
1c204675e5 | ||
|
|
e449a07a3f | ||
|
|
d4f61b9e69 | ||
|
|
4451d68d62 | ||
|
|
bd4f96134f | ||
|
|
767d94875c | ||
|
|
d284c2175d | ||
|
|
f7ab13952e | ||
|
|
5dcdeddf22 | ||
|
|
1018deda96 | ||
|
|
5a0068d86a | ||
|
|
31629fc975 | ||
|
|
634b36c74b | ||
|
|
521a5c57a2 | ||
|
|
1e5b9fb3ef | ||
|
|
f886e7c2f5 | ||
|
|
91863107d7 | ||
|
|
870b863136 | ||
|
|
9292e48554 | ||
|
|
13abd4c751 | ||
|
|
8e2891d7d7 | ||
|
|
315159e4b6 | ||
|
|
fc61dea9b5 | ||
|
|
08ccac3f66 | ||
|
|
ea27d05c58 | ||
|
|
05255f850f | ||
|
|
c96a107e0a | ||
|
|
e2cb128794 | ||
|
|
24fdff0120 | ||
|
|
8999458a72 | ||
|
|
66b8e8bcf8 | ||
|
|
459d0668a8 | ||
|
|
c68da5898a | ||
|
|
1f3b11a223 | ||
|
|
f1c386714d | ||
|
|
5193ba2e39 | ||
|
|
eb18648a66 | ||
|
|
f0dd7d54c8 | ||
|
|
08edda35e1 | ||
|
|
c8728cace4 | ||
|
|
c7296c2498 | ||
|
|
f82d939d15 | ||
|
|
3bb6e08985 | ||
|
|
6104acae8f | ||
|
|
bd4768c147 | ||
|
|
d047ff3b3a | ||
|
|
6edd3f6cf8 | ||
|
|
bdf506a555 | ||
|
|
114289edaf | ||
|
|
274cb74097 | ||
|
|
2d3967872b | ||
|
|
64da400281 | ||
|
|
9ffe1b5ab5 | ||
|
|
83ad72e04b | ||
|
|
653c328e84 | ||
|
|
2bc5e882fd | ||
|
|
45f6a62989 | ||
|
|
21a7202587 | ||
|
|
02cd3b1a18 | ||
|
|
a10ff5930f | ||
|
|
2b4880c784 | ||
|
|
017c3ae36a | ||
|
|
c4b52cb2a3 | ||
|
|
5f2ca58d62 | ||
|
|
1c7212014b | ||
|
|
97dd411ab8 | ||
|
|
d2ad10af9a | ||
|
|
2f06a2b1d3 | ||
|
|
1ac30ed09c | ||
|
|
3603b33a42 | ||
|
|
460fa8e8a9 | ||
|
|
9e09a962af | ||
|
|
3e62a7f5b7 | ||
|
|
b4f833740e | ||
|
|
1fbf4ac08c | ||
|
|
da4c5f48a1 | ||
|
|
1192e8cdad | ||
|
|
bad397e2e6 | ||
|
|
0f15a94b08 | ||
|
|
012614562f | ||
|
|
8b626ea9d4 | ||
|
|
58d22b72ec | ||
|
|
6d552f4680 | ||
|
|
e821d2995d | ||
|
|
cc4eca2563 | ||
|
|
04c344e2a0 | ||
|
|
7f777a4472 | ||
|
|
1f792a9e3c | ||
|
|
6d4e9c7d30 | ||
|
|
9433d6177a | ||
|
|
723805d395 | ||
|
|
c553872f90 | ||
|
|
41756b06e1 | ||
|
|
9e36a2cbb1 | ||
|
|
f0b9a11482 | ||
|
|
f6d21585ab | ||
|
|
b0fab2453a | ||
|
|
b2d6645f46 | ||
|
|
cb902362b5 | ||
|
|
0a2b6494cc | ||
|
|
0c935e8922 | ||
|
|
17f91f7f53 | ||
|
|
d74c2e8466 | ||
|
|
070c3d73cc | ||
|
|
7433327d75 | ||
|
|
a976475617 | ||
|
|
f37b331630 | ||
|
|
4b0ea63e0a | ||
|
|
9dbf498322 | ||
|
|
8e6efac6db | ||
|
|
579e74dd5d | ||
|
|
cad07434ff | ||
|
|
f40fbaf602 | ||
|
|
a43d29d446 | ||
|
|
2f136800c7 | ||
|
|
dd6e9444e1 | ||
|
|
7670f521a2 | ||
|
|
cafb0e02a3 | ||
|
|
9d399c475c | ||
|
|
8766a936f3 | ||
|
|
968b395bce | ||
|
|
0a931bbf77 | ||
|
|
17181db8a5 | ||
|
|
41f1c3f7f7 | ||
|
|
c4406c2a33 | ||
|
|
7f18821990 | ||
|
|
8fe3caf2ea | ||
|
|
15119bec3c | ||
|
|
57e508f331 | ||
|
|
b936d54a48 | ||
|
|
5c8dd5676c | ||
|
|
689558b8c7 | ||
|
|
67001dd99f | ||
|
|
3c3e9a4113 | ||
|
|
d08a1c3ddb | ||
|
|
a77acb7dfb | ||
|
|
ba5ab21b37 | ||
|
|
ce9ff67b24 | ||
|
|
5bbad01909 | ||
|
|
01895bafc0 | ||
|
|
f1ec53d1bc | ||
|
|
d3109a03b2 | ||
|
|
681ddb5af2 | ||
|
|
29061aa088 | ||
|
|
dad21cadb3 | ||
|
|
7e2d627d3f | ||
|
|
cc18ad9a7f | ||
|
|
b2f97a263d | ||
|
|
8b9e45ad31 | ||
|
|
89e013e2e2 | ||
|
|
59a69f0253 | ||
|
|
39fff235d1 | ||
|
|
7316661eb5 | ||
|
|
f6056426dc | ||
|
|
d48f8dceb5 | ||
|
|
abb1a40a4f | ||
|
|
121f56f44b | ||
|
|
b49e3b8f84 | ||
|
|
9bf6b12904 | ||
|
|
6aaa106aff | ||
|
|
576ddbf673 | ||
|
|
d415e81add | ||
|
|
3223d3b74d | ||
|
|
7f3d32a876 | ||
|
|
6d7b596854 | ||
|
|
b2b123b41e | ||
|
|
99cd685194 | ||
|
|
7e23d2c234 | ||
|
|
b4080be5be | ||
|
|
22af44211d | ||
|
|
a4d02adb22 | ||
|
|
72a82c41ee | ||
|
|
450a9495ec | ||
|
|
c0829194dc | ||
|
|
dfaf029a68 | ||
|
|
3f1863151a | ||
|
|
676d640e70 | ||
|
|
c7d1ba1944 | ||
|
|
07a70311df | ||
|
|
c16471d54f | ||
|
|
9a6bc9fec5 | ||
|
|
1888c698f1 | ||
|
|
6518ae8fa3 | ||
|
|
afce4c81a8 | ||
|
|
28223421b1 | ||
|
|
56fd18241d | ||
|
|
7648207ae4 | ||
|
|
9a2fbba956 | ||
|
|
a327c46229 | ||
|
|
720dc9ab05 | ||
|
|
25fa392dda | ||
|
|
ad3297ff2f | ||
|
|
ab27d1c193 | ||
|
|
8160425672 | ||
|
|
68ccec6b9c | ||
|
|
bf34c2e320 | ||
|
|
54bcb33a0e | ||
|
|
6d15f60c5c | ||
|
|
239a829c20 | ||
|
|
4dbf332a56 | ||
|
|
539bf7bec8 | ||
|
|
c6eaf894a4 | ||
|
|
5eef584c7d | ||
|
|
f4a39aba00 | ||
|
|
3300d20c1c | ||
|
|
edcafa7275 | ||
|
|
e8a72c8c7d | ||
|
|
73a99de55c | ||
|
|
3b6403b2f2 | ||
|
|
c48531c586 | ||
|
|
0916757eb8 | ||
|
|
0b299344a7 | ||
|
|
13b610c140 | ||
|
|
7558c2b3e4 | ||
|
|
ddd18e10e0 | ||
|
|
a755882100 | ||
|
|
6586bef84c | ||
|
|
e0a3fafbd5 | ||
|
|
3804ab532d | ||
|
|
158678c2db | ||
|
|
2cceb281c2 | ||
|
|
14de3ba5de | ||
|
|
ac3ee4c317 | ||
|
|
69f723c8f5 | ||
|
|
b6b0d6a9be | ||
|
|
f992347fc5 | ||
|
|
e20444983d | ||
|
|
827246ed4c | ||
|
|
f58c45f7be | ||
|
|
84fa7b5f17 | ||
|
|
8e9ad7d645 | ||
|
|
825edadc13 | ||
|
|
1c20f519a9 | ||
|
|
0848893f1e | ||
|
|
3d705dbeaa | ||
|
|
22b7b84a45 | ||
|
|
6a6d0cf279 | ||
|
|
a39e0a1db9 | ||
|
|
f4beccddae | ||
|
|
8b081cd6b0 | ||
|
|
209200dc4f | ||
|
|
d0ef1e715e | ||
|
|
bf9bb1b973 | ||
|
|
30efec1b09 | ||
|
|
4893c4664d | ||
|
|
47040a61c8 | ||
|
|
da113b99af | ||
|
|
146ddc3ee0 | ||
|
|
d33ae9d9d9 | ||
|
|
d51b3eff6a | ||
|
|
ec45c56868 | ||
|
|
e1b23a7cb4 | ||
|
|
76df1de634 | ||
|
|
5a92972120 | ||
|
|
3f89701b84 | ||
|
|
b0eace6ad8 | ||
|
|
b4e1ac1953 | ||
|
|
a308000d2e | ||
|
|
e1163895e2 | ||
|
|
2f638f5938 | ||
|
|
1b6fd4d13a | ||
|
|
fb6a187639 | ||
|
|
0f64d39cc5 | ||
|
|
5e1eff19b7 | ||
|
|
8da8c14ace | ||
|
|
6cee62696c | ||
|
|
11fa3e08e9 | ||
|
|
2d0be8f996 | ||
|
|
b97d5b0960 | ||
|
|
08e1788426 | ||
|
|
160b01ec12 | ||
|
|
17ddb3bbbd | ||
|
|
ef1bbc29b5 | ||
|
|
ffa7e58025 | ||
|
|
18fbfb449d | ||
|
|
cb57002682 | ||
|
|
722863428d | ||
|
|
99b4c66b5e | ||
|
|
cbc000696e | ||
|
|
0c9e24dc59 | ||
|
|
18fd04d63c | ||
|
|
27a1849b1d | ||
|
|
32fd9bb42f | ||
|
|
15f6d5c9c0 | ||
|
|
2b3551f1fc | ||
|
|
e57121a780 | ||
|
|
ec8106e43d | ||
|
|
a5f9735906 | ||
|
|
71a3079221 | ||
|
|
9b6696bb6e | ||
|
|
01558c985a | ||
|
|
bf8fa95597 | ||
|
|
94462bddb3 | ||
|
|
1920a0f03d | ||
|
|
f74422e4bb | ||
|
|
3120cef335 | ||
|
|
61a8d95f46 | ||
|
|
19051d36dc | ||
|
|
7910292e0f | ||
|
|
26607bc327 | ||
|
|
e7f38ec894 | ||
|
|
d80cbe270a | ||
|
|
818ead85ca | ||
|
|
7f9ce57318 | ||
|
|
3639b190e3 | ||
|
|
034f0a02b1 | ||
|
|
30c4fc9514 | ||
|
|
91601cce9e | ||
|
|
a4fa15a7de | ||
|
|
d4ed6a97a5 | ||
|
|
0db8482873 | ||
|
|
8bccaa6ee2 | ||
|
|
2ae3bec335 | ||
|
|
da5b9be5ca | ||
|
|
d1fbbd3213 | ||
|
|
688a836bbe | ||
|
|
83c49ae78f | ||
|
|
e1a8f4910f | ||
|
|
5ddcc994de | ||
|
|
5446196471 | ||
|
|
0f9ae9efbb | ||
|
|
4aef5b8229 | ||
|
|
6903e05af1 | ||
|
|
43a0c4fe51 | ||
|
|
8342ba68c0 | ||
|
|
48ef5c421b | ||
|
|
290872dcad | ||
|
|
d4fa082e17 | ||
|
|
e6537699fd | ||
|
|
1d950c4f49 | ||
|
|
5e197fb0db | ||
|
|
594ad0e14f | ||
|
|
d73d6e943b | ||
|
|
eb8b02d88b | ||
|
|
c8728e52d8 | ||
|
|
8537e3e135 | ||
|
|
cd1d690fd3 | ||
|
|
67e953902b | ||
|
|
5ffd1f99fd | ||
|
|
cc1a339142 | ||
|
|
1c78526f86 | ||
|
|
b0a5289a42 | ||
|
|
477d23ea37 | ||
|
|
72c2290300 | ||
|
|
61014c747e | ||
|
|
b2ba1f3ca5 | ||
|
|
7c154abf70 | ||
|
|
d012880a90 | ||
|
|
3ce07cf969 | ||
|
|
24dd7b6347 | ||
|
|
9237c9dcee | ||
|
|
143cabe5fc | ||
|
|
370cdc275a | ||
|
|
235cbe12ee | ||
|
|
26a169d938 | ||
|
|
4e67749eef | ||
|
|
e120fd56a6 | ||
|
|
51b9e3732f | ||
|
|
f3a50610af | ||
|
|
4141ae517f | ||
|
|
29095a869c | ||
|
|
cda22a6f0d | ||
|
|
8f4d8b1c02 | ||
|
|
b502b06e82 | ||
|
|
7d541ee916 | ||
|
|
9b14244363 | ||
|
|
b680a42425 | ||
|
|
cf07a6ebb7 | ||
|
|
dd6c82b168 | ||
|
|
18169251fa | ||
|
|
8ee1e468b8 | ||
|
|
1719eee264 | ||
|
|
ae94948246 | ||
|
|
f19bcc48f6 | ||
|
|
41633be1aa | ||
|
|
d2620a655c | ||
|
|
24c1a931a5 | ||
|
|
31025b8c52 | ||
|
|
25f3d5bf02 | ||
|
|
e21bea6ec0 | ||
|
|
0399074fcb | ||
|
|
edc631ad60 | ||
|
|
ff374b7141 | ||
|
|
458989328e | ||
|
|
3c8d6fbe1b | ||
|
|
a052ac8117 | ||
|
|
94a022e76b | ||
|
|
831ac60e25 | ||
|
|
56e648e924 | ||
|
|
61c5eede94 | ||
|
|
2ec0ab871a | ||
|
|
695e4da85e | ||
|
|
2aa9aafdf6 | ||
|
|
071d1922d0 | ||
|
|
95e64d7809 | ||
|
|
5418d0b44c | ||
|
|
bcc065eb8f | ||
|
|
a320b9e05e | ||
|
|
b913bce398 | ||
|
|
c11374ae39 | ||
|
|
eebd54a4e8 | ||
|
|
92ebe85a3f | ||
|
|
25b8108af0 | ||
|
|
8a2b008fbf | ||
|
|
753440faff | ||
|
|
be09db059d | ||
|
|
cbb14d140e | ||
|
|
f06cf887e1 | ||
|
|
194a698372 | ||
|
|
5a5d5bdd51 | ||
|
|
992af0f1cb | ||
|
|
ac3553babb | ||
|
|
97fb2a4fe6 | ||
|
|
709552f14c | ||
|
|
365750565d | ||
|
|
66a2262ee9 | ||
|
|
067426d5e0 | ||
|
|
87eb1914fb | ||
|
|
135899608e | ||
|
|
7720e9f3fd | ||
|
|
61a481f427 | ||
|
|
3fa5ce5404 | ||
|
|
0983f294b7 | ||
|
|
4e7b665ea8 | ||
|
|
b34c5dde40 | ||
|
|
9de2ff2052 | ||
|
|
a4ad9b49b9 | ||
|
|
90055f7680 | ||
|
|
2e31cf51f3 | ||
|
|
4714d5d42f | ||
|
|
5984b0b579 | ||
|
|
d700c448c6 | ||
|
|
ef8599b836 | ||
|
|
dd5b1097f2 | ||
|
|
f7c35fde2d | ||
|
|
4fbb4dfb55 | ||
|
|
84af0405d9 | ||
|
|
c0e5e5f896 | ||
|
|
12bc63fbc9 | ||
|
|
7bbb1d24ac | ||
|
|
49f6ed524d | ||
|
|
7ddfa2d25e | ||
|
|
d3cdfc1b9d | ||
|
|
1f9dbb6bdc | ||
|
|
10eb13920b | ||
|
|
a57dac0706 | ||
|
|
e0179ea332 | ||
|
|
c931d3179b | ||
|
|
c5666f1357 | ||
|
|
b60e5a7ee3 | ||
|
|
c940a4c0fb | ||
|
|
3f72ce4b1f | ||
|
|
5613dbb28b | ||
|
|
2ae57a7970 | ||
|
|
610c9c5149 | ||
|
|
caec9709ef | ||
|
|
a4504dc0c7 | ||
|
|
0fbd9843bd | ||
|
|
240c7db416 | ||
|
|
915873135e | ||
|
|
a822f667af | ||
|
|
c234b5b2d5 | ||
|
|
694899799b | ||
|
|
eb5a9dd20f | ||
|
|
6015f2423b | ||
|
|
01e50d592e | ||
|
|
0524e8dddf | ||
|
|
ab62bfca7e | ||
|
|
7833fc8c80 | ||
|
|
9c2c38c74a | ||
|
|
78700cd2f5 | ||
|
|
695dc96ce8 | ||
|
|
f1d4cae20b | ||
|
|
9518672ad2 | ||
|
|
b055144908 | ||
|
|
7825c107ab | ||
|
|
dd5dd9d7e1 | ||
|
|
63fd3cfff2 | ||
|
|
a6825ac91e | ||
|
|
8ee9d02ffc | ||
|
|
008be6a8e3 | ||
|
|
1f544b98ab | ||
|
|
42e4c64d06 | ||
|
|
548d7ef99a | ||
|
|
9c7bd7d285 | ||
|
|
a79c3dd156 | ||
|
|
fe01f92545 | ||
|
|
8a5d0b2d92 | ||
|
|
f3647ea46d | ||
|
|
31bded8953 | ||
|
|
3f7cb4a458 | ||
|
|
0869931e67 | ||
|
|
948214cacb | ||
|
|
df94d11f53 | ||
|
|
ffed28c9c7 | ||
|
|
25cb47d2f5 | ||
|
|
0979480103 | ||
|
|
dc4c37afb6 | ||
|
|
87bbb6afde | ||
|
|
8916221bba | ||
|
|
8658675f67 | ||
|
|
be3f94d86c | ||
|
|
17657deb0e | ||
|
|
49861b6a84 | ||
|
|
3da33a57e2 | ||
|
|
9c6c6d6b4c | ||
|
|
dbdb8a58fe | ||
|
|
b670259ab6 | ||
|
|
0907f5021e | ||
|
|
301e02bcd8 | ||
|
|
70da177ed7 | ||
|
|
92d854b971 | ||
|
|
d80fec6e60 | ||
|
|
4c06257070 | ||
|
|
d56f030dc4 | ||
|
|
775cd523eb | ||
|
|
a8fa4d2f0c | ||
|
|
2707012325 | ||
|
|
c5ab49c515 | ||
|
|
fe17bec752 | ||
|
|
5b4ce709af | ||
|
|
875aaa0029 | ||
|
|
589b6d7cfe | ||
|
|
649509dec1 | ||
|
|
4d96978b4f | ||
|
|
81ec121918 | ||
|
|
a448fd02e2 | ||
|
|
96d32f2e3e | ||
|
|
4d71c2d1ff | ||
|
|
6ef33eb0c3 | ||
|
|
046c7e21e9 | ||
|
|
47d87e38a6 | ||
|
|
2ab8e92bf4 | ||
|
|
150e1030c3 | ||
|
|
86c5f0d3d3 | ||
|
|
d964221689 | ||
|
|
87eed2e59b | ||
|
|
a737564a6c | ||
|
|
6374a9772c | ||
|
|
b348f7f1ce | ||
|
|
a0c9e3d117 | ||
|
|
3eb8a702c8 | ||
|
|
806d38bbb2 | ||
|
|
84613e51d8 | ||
|
|
0e4804b59f | ||
|
|
69767e978d | ||
|
|
baba65ad43 | ||
|
|
d0460d8691 | ||
|
|
a7476222a9 | ||
|
|
61ac9129b0 | ||
|
|
3aea994101 | ||
|
|
8e0afb2cc4 | ||
|
|
c1c9c7b68a | ||
|
|
6e843533cb | ||
|
|
d8a229c09b | ||
|
|
2494fa5846 | ||
|
|
984391e2b2 | ||
|
|
72ad4b44ce | ||
|
|
1bc88e5b11 | ||
|
|
c289d6a4c5 | ||
|
|
7c4aefd959 | ||
|
|
c1a74aebc5 | ||
|
|
eae580e51f | ||
|
|
e28f7a3bec | ||
|
|
be99eb82e8 | ||
|
|
5af97c969b | ||
|
|
7dfde3a3c5 | ||
|
|
cd22c39078 | ||
|
|
32a5aec34e | ||
|
|
fea76178ee | ||
|
|
3abe2196dd | ||
|
|
18e0fee1a7 | ||
|
|
954814da65 | ||
|
|
180af7e0bd | ||
|
|
79971d677d | ||
|
|
1e835d2fa9 | ||
|
|
76f72dfb58 | ||
|
|
5ae2711c6e | ||
|
|
39ae4a3a10 | ||
|
|
961ecb3ee8 | ||
|
|
205c11dfba | ||
|
|
c743715459 | ||
|
|
b4bd923304 | ||
|
|
e89d3b3807 | ||
|
|
6e69c018b4 | ||
|
|
aff37092bf | ||
|
|
5745c8cc4a | ||
|
|
a5e487441f | ||
|
|
c588436d55 | ||
|
|
82412831d5 | ||
|
|
aa08f0aa48 | ||
|
|
32d12b7f78 | ||
|
|
3b30eb3278 | ||
|
|
99c36f2a2c | ||
|
|
db8b0022fc | ||
|
|
bd882c8bef | ||
|
|
8d358a7a68 | ||
|
|
8731e0816d | ||
|
|
074cc2f26e | ||
|
|
6eaaddd8ed | ||
|
|
419876b575 | ||
|
|
2635217421 | ||
|
|
c6fc3dfe91 | ||
|
|
10aa8c40a7 | ||
|
|
f70abec5ef | ||
|
|
69f2bf664b | ||
|
|
dde9ffb2ae | ||
|
|
47090eb0f7 | ||
|
|
c5d625e261 | ||
|
|
8d7759d22f | ||
|
|
0d4d64c274 | ||
|
|
ea3f8af161 | ||
|
|
a75e1db970 | ||
|
|
072da114db | ||
|
|
968b1b4cb6 | ||
|
|
afb923737c | ||
|
|
79c8f2275c | ||
|
|
d9278c2c24 | ||
|
|
5b0f897118 | ||
|
|
41337d28c3 | ||
|
|
1d29c52a43 | ||
|
|
53e97dac40 | ||
|
|
8716d9c725 | ||
|
|
f278b735cc | ||
|
|
2a65e81316 | ||
|
|
f437d3f883 | ||
|
|
1fd0f8fdde | ||
|
|
61eadf6891 | ||
|
|
3448f3eb0a | ||
|
|
05a5e9cc69 | ||
|
|
34a0a37b63 | ||
|
|
1159d9494c | ||
|
|
92f396761c | ||
|
|
f1dfba6a93 | ||
|
|
b36d70987d | ||
|
|
315af35296 | ||
|
|
97b8bcd5ca | ||
|
|
6b6fdcd5fd | ||
|
|
3d4f79ca59 | ||
|
|
ccf3851d81 | ||
|
|
97d7e6cb9b | ||
|
|
fd945513ac | ||
|
|
92251f2a45 | ||
|
|
51c530c123 | ||
|
|
96bc1cd8f1 | ||
|
|
374dfbdac2 | ||
|
|
534321d1aa | ||
|
|
69c6f81c31 | ||
|
|
9e3d740b43 | ||
|
|
5cdc80ec11 | ||
|
|
217586b536 | ||
|
|
2abec15691 | ||
|
|
688bfa556c | ||
|
|
589d0d0fac | ||
|
|
3c00c578c3 | ||
|
|
eb5f0fcf68 | ||
|
|
c0de35a683 | ||
|
|
d19b47a427 | ||
|
|
573b3a8743 | ||
|
|
5d3f9b4a03 | ||
|
|
c2aab50c7b | ||
|
|
13f34a8bdd | ||
|
|
f88c149076 | ||
|
|
6a510dad6c | ||
|
|
cdb1602e06 | ||
|
|
80baecc8be | ||
|
|
73f0199dc0 | ||
|
|
6b161d5971 | ||
|
|
67ecc108bd | ||
|
|
df380c343e | ||
|
|
6164896793 | ||
|
|
45dcb26123 | ||
|
|
bb9a067293 | ||
|
|
961abad14b | ||
|
|
17ade287ab | ||
|
|
7513474366 | ||
|
|
303659cb0e | ||
|
|
4e1f9c97a5 | ||
|
|
4531157c72 | ||
|
|
26a8bce2c3 | ||
|
|
3383b2b535 | ||
|
|
9960a6cd21 | ||
|
|
0446f449e9 | ||
|
|
a62a2178d0 | ||
|
|
f038423ce2 | ||
|
|
a3683f184e | ||
|
|
abe09e2a85 | ||
|
|
3efad338eb | ||
|
|
29b3ef70ef | ||
|
|
73f5891f87 | ||
|
|
081af2f953 | ||
|
|
0c8922e30f | ||
|
|
82610163cb | ||
|
|
ba82c395f2 | ||
|
|
0954f6d7e8 | ||
|
|
b99b88a30f | ||
|
|
59727656c3 | ||
|
|
0c611b6429 | ||
|
|
2829445f4f | ||
|
|
0cda4e0905 | ||
|
|
aa02c211fe | ||
|
|
6b6105491c | ||
|
|
45df58114c | ||
|
|
a04d772501 | ||
|
|
820d608b18 | ||
|
|
b54c973d82 | ||
|
|
0234ff0252 | ||
|
|
6856fad0c0 | ||
|
|
d40aa49d8c | ||
|
|
05e961f29f | ||
|
|
474f7e0f68 | ||
|
|
fd52a85dbb | ||
|
|
d9866e1f38 | ||
|
|
0d25a8f5b7 | ||
|
|
b4c6499139 | ||
|
|
d8143a67cd | ||
|
|
e3c620e138 | ||
|
|
acf51238d0 | ||
|
|
e7fddf80ae | ||
|
|
f9b49605e4 | ||
|
|
0dec647116 | ||
|
|
3201853cdf | ||
|
|
e83ecb1254 | ||
|
|
2522f0d80f | ||
|
|
70a6ddc897 | ||
|
|
b17a12662c | ||
|
|
be1a374b14 | ||
|
|
49f007601f | ||
|
|
74cdd2d0f3 | ||
|
|
15dcc760b4 | ||
|
|
d2197d99c2 | ||
|
|
badd319bb4 | ||
|
|
e627b14e55 | ||
|
|
4b7419559c | ||
|
|
2c64b78487 | ||
|
|
5e33c2dc6b | ||
|
|
ebb30424fa | ||
|
|
216163f436 | ||
|
|
d3e9c8c9c0 | ||
|
|
cfef374454 | ||
|
|
0d4ca9717e | ||
|
|
7ab47916f2 | ||
|
|
283c53fddf | ||
|
|
c5c11864e0 | ||
|
|
da56310db9 | ||
|
|
7935c00338 | ||
|
|
1620f6a311 | ||
|
|
4925f3227a | ||
|
|
c158edb574 | ||
|
|
ab18fe466b | ||
|
|
88bb620af2 | ||
|
|
a65d5269fe | ||
|
|
1f3d763490 | ||
|
|
184ac728db | ||
|
|
5bf414ffb5 | ||
|
|
22163173fe | ||
|
|
de0f8c24f7 | ||
|
|
f946a4bfb3 | ||
|
|
18341717a1 | ||
|
|
d2e989403f | ||
|
|
af713672fb | ||
|
|
db2d00f828 | ||
|
|
515406c05d | ||
|
|
61d1d9ec90 | ||
|
|
e020aaa368 | ||
|
|
771cf39944 | ||
|
|
c7f3aaa654 | ||
|
|
220892824c | ||
|
|
ce5950ca73 | ||
|
|
24e31a69cb | ||
|
|
65461a09a7 | ||
|
|
6c7e51041f | ||
|
|
98757aa428 | ||
|
|
9a81057d95 | ||
|
|
f00da20764 | ||
|
|
d323c9df88 | ||
|
|
4e2a41f4cf | ||
|
|
8a5a4f3362 | ||
|
|
8cdc4674d7 | ||
|
|
acfe8950b8 | ||
|
|
502c7e756b | ||
|
|
7fd224e690 | ||
|
|
dc4d388d9a | ||
|
|
df281defd8 | ||
|
|
7d3474aeea | ||
|
|
343f139904 | ||
|
|
b95c1deee1 | ||
|
|
b5abb17568 | ||
|
|
ab2eeb0da3 | ||
|
|
e232962649 | ||
|
|
42012d7bfe | ||
|
|
06b2c623cb | ||
|
|
792908eb35 | ||
|
|
a091036744 | ||
|
|
d9812e2bdb | ||
|
|
74b8ee8c10 | ||
|
|
db5d04c37f | ||
|
|
7984806b38 | ||
|
|
766bd3b76d | ||
|
|
b2444c2aca | ||
|
|
bb8852d57e | ||
|
|
037d9323a4 | ||
|
|
f253d2ea69 | ||
|
|
a4dc27f073 | ||
|
|
812df7b07b | ||
|
|
c237f82e51 | ||
|
|
e7c20f0707 | ||
|
|
0f9bdab108 | ||
|
|
1005fbabbd | ||
|
|
8490583d73 | ||
|
|
5bca783e12 | ||
|
|
6751c5a63a | ||
|
|
00502ce33d | ||
|
|
4c4f598552 | ||
|
|
7cff5cbf61 | ||
|
|
12831a7d21 | ||
|
|
7b99a33a2f | ||
|
|
6d736d7309 | ||
|
|
f558ded5bb | ||
|
|
324bb68667 | ||
|
|
70cae93a4b | ||
|
|
627a5825f4 | ||
|
|
899233338d | ||
|
|
a7e3f9c465 | ||
|
|
6069b8946b | ||
|
|
c58725dbc0 | ||
|
|
292179d41d | ||
|
|
a89ae94d85 | ||
|
|
4fc9274e00 | ||
|
|
5259dd8054 | ||
|
|
d8fe59debb | ||
|
|
409fac3ef1 | ||
|
|
239dd0567f | ||
|
|
62cac20ba7 | ||
|
|
ff30435eb4 | ||
|
|
5a0b119410 | ||
|
|
ccfe9b9d08 | ||
|
|
6f4a832389 | ||
|
|
8acbcb2ed2 | ||
|
|
04151b9957 | ||
|
|
dfc628a397 | ||
|
|
4db352f55b | ||
|
|
d8e6433404 | ||
|
|
d0ba17374e | ||
|
|
7f09b486d9 | ||
|
|
30fd51c268 | ||
|
|
556fb4e09f | ||
|
|
a3096689b5 | ||
|
|
bc232c4f77 | ||
|
|
342e4bdee8 | ||
|
|
dc01833a5b | ||
|
|
170b49428c | ||
|
|
c6f875c517 | ||
|
|
07dd6d1192 | ||
|
|
25d9dbe93c | ||
|
|
8cc09f0e5a | ||
|
|
02b5483d81 | ||
|
|
b8c10a0350 | ||
|
|
126d7fd62c | ||
|
|
3c6f50b788 | ||
|
|
2cb6b15bc3 | ||
|
|
2f5b7ad654 | ||
|
|
6aedfb5219 | ||
|
|
397da7d676 | ||
|
|
e75d33439a | ||
|
|
7241f7509f | ||
|
|
8827fd2d74 | ||
|
|
6fc3bbb97d | ||
|
|
508bda37f5 | ||
|
|
a0ef3cfc34 | ||
|
|
1b9c9c48b6 | ||
|
|
22e20d29f5 | ||
|
|
0604f76f56 | ||
|
|
4855e86a3f | ||
|
|
69349368bf | ||
|
|
b531d9eeb2 | ||
|
|
246fcb8efa | ||
|
|
3972bd3ff3 | ||
|
|
d1c96cd4b2 | ||
|
|
154783a974 | ||
|
|
fa07f4ee8a | ||
|
|
0e8e88fac0 | ||
|
|
426c3c4062 | ||
|
|
8d0334e003 | ||
|
|
0ab015abfd | ||
|
|
7350e150c8 | ||
|
|
61f0205529 | ||
|
|
11d06b5e2b | ||
|
|
a9b5762fbc | ||
|
|
724b1c6fd4 | ||
|
|
acd98365c1 | ||
|
|
32c49c080c | ||
|
|
d371c0c5ae | ||
|
|
47171174b5 | ||
|
|
f274684473 | ||
|
|
d96ac56460 | ||
|
|
77f3a1f146 | ||
|
|
ba3f46fbd6 | ||
|
|
0b406b6988 | ||
|
|
6f0cfd23c1 | ||
|
|
10d51ada37 | ||
|
|
9fa70f6cb8 | ||
|
|
cec5a7f2c6 | ||
|
|
829df26b0f | ||
|
|
b8bebc9b64 | ||
|
|
90ca5a8bb7 | ||
|
|
2c11255828 | ||
|
|
9803dd9547 | ||
|
|
dcff84958f | ||
|
|
3a9cf6c360 | ||
|
|
8ff872f41d | ||
|
|
5940cec0e6 | ||
|
|
5359da3ce2 | ||
|
|
c9369db578 | ||
|
|
d70deaf1ac | ||
|
|
b31f8d5867 | ||
|
|
5350f83275 | ||
|
|
dd80b94b43 | ||
|
|
4cbadbd941 | ||
|
|
e61341df79 | ||
|
|
f7ab26a5da | ||
|
|
0d3bde1191 | ||
|
|
c143735393 | ||
|
|
43af5383e3 | ||
|
|
ffed46175d | ||
|
|
31a39be9ea | ||
|
|
2022ca8e10 | ||
|
|
d0d433db1a | ||
|
|
e8d8f7c406 | ||
|
|
f6b2abb1fb | ||
|
|
011c125564 | ||
|
|
1a9f90a08e | ||
|
|
3eb4897bd3 | ||
|
|
3c9dbaf860 | ||
|
|
d918d5b466 | ||
|
|
d95ffdfbf7 | ||
|
|
363ddb70e2 | ||
|
|
bb76f6c652 | ||
|
|
641e998504 | ||
|
|
0a9e4a5e85 | ||
|
|
8aa92bb688 | ||
|
|
1d5faa3101 | ||
|
|
b97b34961c | ||
|
|
be92ac58ae | ||
|
|
1df0def9ea | ||
|
|
a43ac90b21 | ||
|
|
bbecbc8578 | ||
|
|
6458231946 | ||
|
|
97a2e8bb50 | ||
|
|
400d744938 | ||
|
|
9a6d20b6ec | ||
|
|
7cebc4efe8 | ||
|
|
91f0800e26 | ||
|
|
a99a6b6946 | ||
|
|
c37dea2079 | ||
|
|
b55b516fc3 | ||
|
|
1f3c0d004f | ||
|
|
3c3fda8064 | ||
|
|
ae9bbb40fd | ||
|
|
8e5e788bcd | ||
|
|
c50cdd2976 | ||
|
|
f45690b34f | ||
|
|
f0b0e41d33 | ||
|
|
308aa2eca2 | ||
|
|
3771b2ff70 | ||
|
|
8b0f31c43a | ||
|
|
8ba7b078fd | ||
|
|
57f0b04387 | ||
|
|
24d7cb3d5d | ||
|
|
85cf2169d4 | ||
|
|
0427d406b9 | ||
|
|
96dd2f5c85 | ||
|
|
e355c7b8ef | ||
|
|
7789c8d13d | ||
|
|
6e46a17d98 | ||
|
|
e48f36397e | ||
|
|
cf15b7eaff | ||
|
|
651e89994e | ||
|
|
342298ad3e | ||
|
|
a76191ffdc | ||
|
|
279987925a | ||
|
|
824b1c7f6f | ||
|
|
bcae7beae6 | ||
|
|
9353022627 | ||
|
|
35abf16f7d | ||
|
|
c14eb42186 | ||
|
|
98d6043f2d | ||
|
|
7a8d4a3a59 | ||
|
|
3bf0d4aabb | ||
|
|
edd60cad11 | ||
|
|
e3223f745a | ||
|
|
fcd39370ed | ||
|
|
86ffcc973c | ||
|
|
9493fb07cb | ||
|
|
9303415b89 | ||
|
|
8b8e391feb | ||
|
|
234684c875 | ||
|
|
25f1c9ccf3 | ||
|
|
1dd17b1814 | ||
|
|
089b54986a | ||
|
|
224797e5e7 | ||
|
|
346fbcc286 | ||
|
|
1ee53c6877 | ||
|
|
7f4d737503 | ||
|
|
551d3df892 | ||
|
|
b2bfdb097b | ||
|
|
40572cdc00 | ||
|
|
46f0d17da7 | ||
|
|
bc474c6c06 | ||
|
|
31a41000bf | ||
|
|
a5621a4178 | ||
|
|
17869dc5d8 | ||
|
|
69c1639e46 | ||
|
|
ffcd669898 | ||
|
|
8121e48825 | ||
|
|
aed18698a3 | ||
|
|
e1d5cbd06e | ||
|
|
964247b1f2 | ||
|
|
ef604615ec | ||
|
|
bd3735e755 | ||
|
|
7bb6890370 | ||
|
|
734f6564fa | ||
|
|
b5c159c967 | ||
|
|
ba215335bf | ||
|
|
da4bd937a8 | ||
|
|
b5b14373d0 | ||
|
|
270499a4fd | ||
|
|
26ad623d0e | ||
|
|
f8eedc8650 | ||
|
|
89e32b327d | ||
|
|
27223a1883 | ||
|
|
e0ae7eeee7 | ||
|
|
b5fefb4687 | ||
|
|
ecfb2f3d40 | ||
|
|
1614dd5a4d | ||
|
|
cf3e89c374 | ||
|
|
0f0908d3f3 | ||
|
|
31c3def1a8 | ||
|
|
f75003461a | ||
|
|
49504e46ee | ||
|
|
699bdd1348 | ||
|
|
e608fbaad5 | ||
|
|
6a7d105484 | ||
|
|
6acda5fcd1 | ||
|
|
57ba8ed2fb | ||
|
|
d6c4017a2e | ||
|
|
b7be5d14e0 | ||
|
|
f130a78f0a | ||
|
|
a077053b68 | ||
|
|
c93c8de7fe | ||
|
|
3176156639 | ||
|
|
7072d16f00 | ||
|
|
f2dda35f28 | ||
|
|
0cc04ee20d | ||
|
|
7531a3ada7 | ||
|
|
08717d196f | ||
|
|
a1859676e4 | ||
|
|
e4a54ddbf8 | ||
|
|
5b9a9779c8 | ||
|
|
32ab8a1646 | ||
|
|
f0e943ebcc | ||
|
|
c0e91896df | ||
|
|
d60562a034 | ||
|
|
93e08a6e29 | ||
|
|
0f09172ed0 | ||
|
|
948763443e | ||
|
|
3ef3b452e2 | ||
|
|
39ebdb2f61 | ||
|
|
dff50305de | ||
|
|
f71c8551e8 | ||
|
|
d63d4eb019 | ||
|
|
d66ba9d6c6 | ||
|
|
8526437c88 | ||
|
|
987b1c2c36 | ||
|
|
18e159350b | ||
|
|
1338d25b4e | ||
|
|
f994c4d1da | ||
|
|
5fab276c26 | ||
|
|
9f171da570 | ||
|
|
fed00d04a6 | ||
|
|
ecfaf9f02d | ||
|
|
d05e9d0b45 | ||
|
|
d4385c7e43 | ||
|
|
04fc9962ff | ||
|
|
2fd68845ce | ||
|
|
bd69339e22 | ||
|
|
c6404f7ed6 | ||
|
|
6ce948366d | ||
|
|
9e78fd3651 | ||
|
|
5afd135967 | ||
|
|
d5aa9324fa | ||
|
|
9096a6e5b8 | ||
|
|
cb58012a82 | ||
|
|
58bb3cc84f | ||
|
|
c2ff05201a | ||
|
|
9be13cf08f | ||
|
|
eb4ec47f7a | ||
|
|
e2eb9b72f8 | ||
|
|
c9ff235089 | ||
|
|
9af809a4f0 | ||
|
|
288a42663a | ||
|
|
cca15d4211 | ||
|
|
fe2081b407 | ||
|
|
fa323d4987 | ||
|
|
eeef4a2f95 | ||
|
|
2e49f51093 | ||
|
|
0481e83ec4 | ||
|
|
ff8b5bd6c0 | ||
|
|
aabab653d3 | ||
|
|
efe0b3acc0 | ||
|
|
35cc966132 | ||
|
|
777997202b | ||
|
|
1f09a40c77 | ||
|
|
d20fecadac | ||
|
|
fa430bf104 | ||
|
|
8bb66ac254 | ||
|
|
6518aa3670 | ||
|
|
c76d9ebd88 | ||
|
|
7f4d3ffdbc | ||
|
|
b908fdafc6 | ||
|
|
ef59cff44b | ||
|
|
f511802db5 | ||
|
|
30c74b8427 | ||
|
|
2dd16b91ec | ||
|
|
bb7a3ea053 | ||
|
|
0499a7265a | ||
|
|
1959c685b9 | ||
|
|
cd80fbcdbb | ||
|
|
5814b833ed | ||
|
|
cda0b9c90a | ||
|
|
609bba569e | ||
|
|
77c7f8fb54 | ||
|
|
f65290ef38 | ||
|
|
de594aebd5 | ||
|
|
2be81c0322 | ||
|
|
e902fccd39 | ||
|
|
61d162312f | ||
|
|
30a3cd2911 | ||
|
|
2dfe9337a2 | ||
|
|
c188696328 | ||
|
|
f8fac06e1b | ||
|
|
edad26e05b | ||
|
|
6c8117045f | ||
|
|
b573170d69 | ||
|
|
1d14f08541 | ||
|
|
489d796d7b | ||
|
|
1efede4de8 | ||
|
|
a1187757bc | ||
|
|
263ff1ee08 | ||
|
|
2f2289a863 | ||
|
|
e07305ef46 | ||
|
|
1c61ed6a8b | ||
|
|
cf1da2a420 | ||
|
|
7de03c0fc7 | ||
|
|
9c38f39e5b | ||
|
|
0937e64c2e | ||
|
|
5a6e0283dd | ||
|
|
5489797900 | ||
|
|
6e48fe6357 | ||
|
|
485b45675d | ||
|
|
d31f882f1a | ||
|
|
209c078614 | ||
|
|
d2b4594bc2 | ||
|
|
6fcfb385cd | ||
|
|
b12b83cbb5 | ||
|
|
26804b4093 | ||
|
|
8ebcd47599 | ||
|
|
d810a082c5 | ||
|
|
2f9255a46f | ||
|
|
5d0171d5b7 | ||
|
|
c5382c88a2 | ||
|
|
98f9e632ac | ||
|
|
f912869de6 | ||
|
|
8cd6d23ff2 | ||
|
|
f3d863ea45 | ||
|
|
958909551d | ||
|
|
b132a0c0b5 | ||
|
|
6e7e4d4742 | ||
|
|
c95c76d0e0 | ||
|
|
8735af4cbb | ||
|
|
edfef5822f | ||
|
|
ebef54ca44 | ||
|
|
c937abe098 | ||
|
|
587c9b2c3f | ||
|
|
74ac1dc06b | ||
|
|
0707b68796 | ||
|
|
445a83f78a | ||
|
|
bc1d89da89 | ||
|
|
dc5751951e | ||
|
|
3ca6629175 | ||
|
|
1e1aa67b3e | ||
|
|
3dfd9cd512 | ||
|
|
6a51bd1a1c | ||
|
|
5e329f51a4 | ||
|
|
d86cebf99a | ||
|
|
9b43c6c238 | ||
|
|
1084be4712 | ||
|
|
44b2bcb759 | ||
|
|
2d1e001ddf | ||
|
|
8e8d8c9d6a | ||
|
|
a1862d912a | ||
|
|
fd713e0e5c | ||
|
|
d7442b4879 | ||
|
|
4f79e909e5 | ||
|
|
69061791ed | ||
|
|
38f15c976f | ||
|
|
b745ebdda1 | ||
|
|
ae58fd548f | ||
|
|
fbc3078c07 | ||
|
|
860c7f1508 | ||
|
|
3fcd9589a4 | ||
|
|
ba5e90abc3 | ||
|
|
5587ff59b0 | ||
|
|
6d329b130a | ||
|
|
357aea1693 | ||
|
|
3ab0c94496 | ||
|
|
b3f83c3362 | ||
|
|
17a7470a2a | ||
|
|
365a5ccd46 | ||
|
|
742a2b1b85 | ||
|
|
388ebe3bee | ||
|
|
6d302eb25a | ||
|
|
fa1f2404d9 | ||
|
|
52d8825d95 | ||
|
|
e743ae3dbe | ||
|
|
db72048c31 | ||
|
|
f1ff52eb6b | ||
|
|
0b05899818 | ||
|
|
bd59b6316e | ||
|
|
2c74c8a8df | ||
|
|
dc2ea0d6bf | ||
|
|
73382cbcfb | ||
|
|
e6254ddc2c | ||
|
|
30607730f2 | ||
|
|
b92c5188d1 | ||
|
|
524cd1d990 | ||
|
|
231f961945 | ||
|
|
267c1cd696 | ||
|
|
a1e947ae1e | ||
|
|
300e106143 | ||
|
|
57e85affde | ||
|
|
34b74b9e4e | ||
|
|
c218949556 | ||
|
|
4b90cd9b82 | ||
|
|
fcb3fd7186 | ||
|
|
4dbc6803fb | ||
|
|
b6af8368b6 | ||
|
|
6e4b291808 | ||
|
|
4c88ea3c05 | ||
|
|
0e5fd68e6c | ||
|
|
1103ee8f52 | ||
|
|
9efa9e979c | ||
|
|
f331b4471f | ||
|
|
f11016f57c | ||
|
|
4b53bae57b | ||
|
|
b2ccbc3f9e | ||
|
|
7dcf050dc4 | ||
|
|
717869fd30 | ||
|
|
598d6cffa4 | ||
|
|
c022c81100 | ||
|
|
5af9dd655e | ||
|
|
abdf9c72ca | ||
|
|
3778724b73 | ||
|
|
d76865c1ed | ||
|
|
3ccf7adac1 | ||
|
|
c0e8ff8620 | ||
|
|
ca75484eb8 | ||
|
|
8adf1c5d71 | ||
|
|
5f35b740df | ||
|
|
b0d1dd9485 | ||
|
|
ade8fb927c | ||
|
|
31708c0d24 | ||
|
|
9b3a51469e | ||
|
|
cdd31227b7 | ||
|
|
b02a45ee6c | ||
|
|
f796e8f673 | ||
|
|
930c57398a | ||
|
|
6b39c9946b | ||
|
|
3a71d7f1a8 | ||
|
|
d48020a919 | ||
|
|
210b3c8588 | ||
|
|
bdf6d97478 | ||
|
|
1ac9bebb8d | ||
|
|
ef3156e6f7 | ||
|
|
95a7557acf | ||
|
|
8e30ffb840 | ||
|
|
20b5facc38 | ||
|
|
f7eda41a54 | ||
|
|
6a0edb63f0 | ||
|
|
e7976e4235 | ||
|
|
2bd3fdb4e7 | ||
|
|
94a4d68be2 | ||
|
|
b879e724e2 | ||
|
|
234f79857c | ||
|
|
f73d34b9cc | ||
|
|
936faad1b9 | ||
|
|
649783d2c3 | ||
|
|
c0bacb7c7c | ||
|
|
6436fab837 | ||
|
|
e38620a2a2 | ||
|
|
702b1bd3b7 | ||
|
|
c2c833f279 | ||
|
|
a816a7b149 | ||
|
|
32519f8eae | ||
|
|
dbb4904513 | ||
|
|
0e7a8c1a62 | ||
|
|
68582ca466 | ||
|
|
b1f0297e82 | ||
|
|
eeff2ab215 | ||
|
|
62fb6429f9 | ||
|
|
90c3ed96e3 | ||
|
|
c79e625000 | ||
|
|
bce3def4c6 | ||
|
|
1437140f87 | ||
|
|
fd19bb6299 | ||
|
|
a401440004 | ||
|
|
06aef8fb1a | ||
|
|
c418f142ba | ||
|
|
88d21caa19 | ||
|
|
f6c58054a6 | ||
|
|
b4d74bc589 | ||
|
|
d7bb4c1005 | ||
|
|
3c610668b5 | ||
|
|
5d28a6e402 | ||
|
|
e886b55727 | ||
|
|
8c552ccc45 | ||
|
|
054edeefb6 | ||
|
|
6c599e0127 | ||
|
|
4f97987061 | ||
|
|
50e82c5b99 | ||
|
|
1f9337feca | ||
|
|
cba53bba55 | ||
|
|
427fda1015 | ||
|
|
a95e1bb835 | ||
|
|
5ab882ae66 | ||
|
|
e5ce5766be | ||
|
|
725405e4fd | ||
|
|
cad7a193b3 | ||
|
|
69ac8ae147 | ||
|
|
247e5e7f24 | ||
|
|
04821a00f8 | ||
|
|
ad3178fe94 | ||
|
|
ccd1a10892 | ||
|
|
b80ad40f54 | ||
|
|
7cf190e3f9 | ||
|
|
cac94245ea | ||
|
|
a729e54425 | ||
|
|
d4b57fc1b0 | ||
|
|
d63b5772e4 | ||
|
|
37e0f80fb8 | ||
|
|
532fbbd4f1 | ||
|
|
16f5906979 | ||
|
|
bf3fb24c3a | ||
|
|
a8eb9f3e79 | ||
|
|
a88d8ca410 | ||
|
|
ba8d808cd9 | ||
|
|
d67aab573d | ||
|
|
5098b1c696 | ||
|
|
0b9b94bc0b | ||
|
|
8aec0c1ac7 | ||
|
|
77f69fbc5e | ||
|
|
14a0c1871f | ||
|
|
49e7796df9 | ||
|
|
318a053dbf | ||
|
|
29953eb7c7 | ||
|
|
1816723f16 | ||
|
|
5fb5c1bb40 | ||
|
|
4d5d56fe79 | ||
|
|
7213c5c637 | ||
|
|
eb161b978a | ||
|
|
23d4c926b7 | ||
|
|
c354b64679 | ||
|
|
9c93d5a8d2 | ||
|
|
4f75f76db8 | ||
|
|
4dfef99216 | ||
|
|
47ce09393c | ||
|
|
92e9f988f3 | ||
|
|
037a88f0f5 | ||
|
|
e8db8addd7 | ||
|
|
d0ba0503e5 | ||
|
|
27be35ae77 | ||
|
|
835706f780 | ||
|
|
f79675b265 | ||
|
|
0a35f757e9 | ||
|
|
856852592d | ||
|
|
f60a896926 | ||
|
|
b6fc8b777f | ||
|
|
caefbdc917 | ||
|
|
c6823be302 | ||
|
|
7bfb1d19fe | ||
|
|
f24b34758c | ||
|
|
d4b1bdef8e | ||
|
|
dee1d31fc0 | ||
|
|
363bce82d8 | ||
|
|
6be7003ac2 | ||
|
|
586e43c8d0 | ||
|
|
18e66f52dd | ||
|
|
09e86f0a6b | ||
|
|
3b5daf19c3 | ||
|
|
a037108cf3 | ||
|
|
b8a8c3ebf3 | ||
|
|
9fa7f8762e | ||
|
|
9ec457bf5f | ||
|
|
2691cc0b6d | ||
|
|
4645029a27 | ||
|
|
b1768565c1 | ||
|
|
7323b72c4b | ||
|
|
2e5c01a4da | ||
|
|
19dd1af4dc | ||
|
|
0e9a962506 | ||
|
|
fa195c3808 | ||
|
|
cd6cfc6ae9 | ||
|
|
02e9ba54f9 | ||
|
|
b79056295b | ||
|
|
8653630d83 | ||
|
|
99f09709ec | ||
|
|
bcb914485d | ||
|
|
0b79f754f9 | ||
|
|
fe87e32e2b | ||
|
|
9c9390878e | ||
|
|
1c2cba64ec | ||
|
|
b69853a608 | ||
|
|
852de35e3e | ||
|
|
6fbb387488 | ||
|
|
36bfb50aad | ||
|
|
28e0ea3e81 | ||
|
|
6ba9191b30 | ||
|
|
b6f82ca020 | ||
|
|
81a6f4841f | ||
|
|
f2487a22cd | ||
|
|
4d0331e105 | ||
|
|
66e53c2701 | ||
|
|
2e6f3d3579 | ||
|
|
80fde52dc5 | ||
|
|
82a88e0b0a | ||
|
|
d3f9fc7a21 | ||
|
|
9d7c30336e | ||
|
|
ff5b339ce8 | ||
|
|
70c86d2a44 | ||
|
|
0924484bc4 | ||
|
|
dd36857cf8 | ||
|
|
2f54c369f7 | ||
|
|
9e5402e2c3 | ||
|
|
097d77755a | ||
|
|
4fbeb5c172 | ||
|
|
23b04fbbd7 | ||
|
|
d9455e3f9b | ||
|
|
beeb2442ad | ||
|
|
f6a228008b | ||
|
|
73522dc4c1 | ||
|
|
d9b4f5504c | ||
|
|
01511c0d5a | ||
|
|
01cf2d4e7b | ||
|
|
e9212d5ce6 | ||
|
|
bc2345ba14 | ||
|
|
c2818870d3 | ||
|
|
38b0f2f5f9 | ||
|
|
79300b752b | ||
|
|
ed6559bbe9 | ||
|
|
0821e7cf41 | ||
|
|
38f72a85e4 | ||
|
|
ccc2bcf066 | ||
|
|
d9d009ab1f | ||
|
|
f268af945f | ||
|
|
34e8b32180 | ||
|
|
c6626e83f2 | ||
|
|
4e5c501041 | ||
|
|
0832ea97b1 | ||
|
|
7bef517518 | ||
|
|
ea973bbb52 | ||
|
|
b673fec532 | ||
|
|
7e185197aa | ||
|
|
5e86b06db8 | ||
|
|
2e2b065c42 | ||
|
|
91e006cfb0 | ||
|
|
b1a36bbb11 | ||
|
|
ac40098ac5 | ||
|
|
6ff030847e | ||
|
|
a12e401f15 | ||
|
|
49f2f92a9c | ||
|
|
d1ac7ca647 | ||
|
|
f6c26a201e | ||
|
|
90b875adae | ||
|
|
7be7772af6 | ||
|
|
2fa1d7a95b | ||
|
|
6e747fa299 | ||
|
|
86cc963673 | ||
|
|
e776c3c5f9 | ||
|
|
33da9fd143 | ||
|
|
09416286bf | ||
|
|
c5f9db450d | ||
|
|
1e93b13b4c | ||
|
|
48cf64cd3a | ||
|
|
ff3666c6af | ||
|
|
3918025ffd | ||
|
|
1b7ee3b575 | ||
|
|
b370f01551 | ||
|
|
18da6f69a2 | ||
|
|
d040e2719f | ||
|
|
9eb64466bc | ||
|
|
9fc0c9da06 | ||
|
|
f63b75c01b | ||
|
|
aa1dea1b08 | ||
|
|
71116bc525 | ||
|
|
8f1e8af2e1 | ||
|
|
c691759af8 | ||
|
|
d604639b96 | ||
|
|
cde3185a20 | ||
|
|
40ae57d7ea | ||
|
|
9efd42d4ed | ||
|
|
3248ee1a3d | ||
|
|
13d325e259 | ||
|
|
79994e13c7 | ||
|
|
3f9a9157f4 | ||
|
|
7adcb11f1d | ||
|
|
42f6392cc4 | ||
|
|
835e32b1cc | ||
|
|
a7e2592fa8 | ||
|
|
f17001e86c | ||
|
|
7113852cd6 | ||
|
|
7d85c0393f | ||
|
|
48af55adab | ||
|
|
c36267dc17 | ||
|
|
b7eb95f85f | ||
|
|
08cbaa1622 | ||
|
|
a73fa7811c | ||
|
|
0d5fa418af | ||
|
|
cf59ebf1aa | ||
|
|
52566cb562 | ||
|
|
75b307105b | ||
|
|
998281d86d | ||
|
|
8494989715 | ||
|
|
b73527fb33 | ||
|
|
f18c68b8ba | ||
|
|
c8c0366b39 | ||
|
|
22c90eecfc | ||
|
|
3695989866 | ||
|
|
80c5acef81 | ||
|
|
2cb6a1aac0 | ||
|
|
39b7a7290f | ||
|
|
7b792907d3 | ||
|
|
c103738302 | ||
|
|
89901fa7fb | ||
|
|
3176608eaa | ||
|
|
94d0f7ebea | ||
|
|
04eb4484da | ||
|
|
9ffc887adf | ||
|
|
f5b97c57f8 | ||
|
|
46c17b055e | ||
|
|
b5bbbf7eb3 | ||
|
|
87f5a531bb | ||
|
|
3afeda8b19 | ||
|
|
832dc0b0e1 | ||
|
|
13cecd9b91 | ||
|
|
cba7195670 | ||
|
|
446a51122f | ||
|
|
e3c0757166 | ||
|
|
0715e11c05 | ||
|
|
1fc86733ba | ||
|
|
4c002afde1 | ||
|
|
caa0e29564 | ||
|
|
0896c9ab3f | ||
|
|
c56c44f8c0 | ||
|
|
655bfc855e | ||
|
|
439efa1c83 | ||
|
|
59591c2a13 | ||
|
|
e452c6d70a | ||
|
|
12967bb6f9 | ||
|
|
6798dd8686 | ||
|
|
d72a8ac5a9 | ||
|
|
f38f809337 | ||
|
|
c78ab8edf7 | ||
|
|
f8213a1a2a | ||
|
|
da7cc102a3 | ||
|
|
ea29305f8b | ||
|
|
9fb71ade71 | ||
|
|
5740dec7b4 | ||
|
|
7c742bf061 | ||
|
|
a30ec0c75e | ||
|
|
b8abedc5b9 | ||
|
|
ef7f42cf6d | ||
|
|
e817715da3 | ||
|
|
9c0825ce70 | ||
|
|
2e81f0cf95 | ||
|
|
c4b099d2a5 | ||
|
|
f3606dfd96 | ||
|
|
05f80f743b | ||
|
|
f249509817 | ||
|
|
c6e0b2007d | ||
|
|
2496f1c654 | ||
|
|
1a32fb5da1 | ||
|
|
be2f0b2172 | ||
|
|
5e5aab7962 | ||
|
|
a8bdc37780 | ||
|
|
3260584f90 | ||
|
|
b58ff14ed1 | ||
|
|
db13b7a3e9 | ||
|
|
3150461d12 | ||
|
|
97bfbe24b8 | ||
|
|
30b5f74c51 | ||
|
|
69aba4643e | ||
|
|
f849ffef3e | ||
|
|
6d5513c69e | ||
|
|
b27e2075ff | ||
|
|
9979fdf6de | ||
|
|
b655b064df | ||
|
|
974619d1ce | ||
|
|
280560f286 | ||
|
|
7ce12af146 | ||
|
|
ba955df493 | ||
|
|
34cbb13b8a | ||
|
|
aa262d0430 | ||
|
|
4294ecaf26 | ||
|
|
014d0935ba | ||
|
|
e65cad5074 | ||
|
|
b0f2b346f9 | ||
|
|
affd9aadb5 | ||
|
|
23051a4a05 | ||
|
|
e9fe871af3 | ||
|
|
80b1bda51c | ||
|
|
e117cd003f | ||
|
|
e03b4722b0 | ||
|
|
b5a3fb44c3 | ||
|
|
4903a17104 | ||
|
|
a068498561 | ||
|
|
1a3541e575 | ||
|
|
49267b57e4 | ||
|
|
d337b929ef | ||
|
|
f74b15c1bf | ||
|
|
d3fd9e05ca | ||
|
|
16b94c1089 | ||
|
|
d6b4c7c485 | ||
|
|
0309213ccc | ||
|
|
e408414631 | ||
|
|
80ffb9d762 | ||
|
|
bd2a3e6119 | ||
|
|
95e8115045 | ||
|
|
b7cfac4d1f | ||
|
|
5be2633795 | ||
|
|
cb12b83e47 | ||
|
|
cd798daf0a | ||
|
|
7f6f46b662 | ||
|
|
e78d7b0219 | ||
|
|
58082179fe | ||
|
|
622611498c | ||
|
|
b346e1c480 | ||
|
|
bf23d6d7aa | ||
|
|
3d0b8917fa | ||
|
|
8723d673d7 | ||
|
|
35c818f4a0 | ||
|
|
532646a431 | ||
|
|
0fd7b980de | ||
|
|
2f0e91a538 | ||
|
|
0f6f3bdb9a | ||
|
|
2c3e681942 | ||
|
|
34ccaeb10f | ||
|
|
8f985ade97 | ||
|
|
9b7ef1139e | ||
|
|
2829a7ad31 | ||
|
|
b5325fd0ac | ||
|
|
69bbdab450 | ||
|
|
6975acfc8a | ||
|
|
9233fef63f | ||
|
|
33663783cc | ||
|
|
6f4fd18c47 | ||
|
|
7decfdc37f | ||
|
|
267bb02417 | ||
|
|
255e3a043a | ||
|
|
6080a861db | ||
|
|
4959073a33 | ||
|
|
f6dbec1436 | ||
|
|
88fbcabcbb | ||
|
|
1f26e49fb8 | ||
|
|
104ee779e7 | ||
|
|
77da01d1ee | ||
|
|
0e6fd89f0b | ||
|
|
dd912e8958 | ||
|
|
e95c47ff6a | ||
|
|
6b7937c639 | ||
|
|
f72c5753c9 | ||
|
|
bf98aa5464 | ||
|
|
f3102e3b5b | ||
|
|
e0aaba2cf5 | ||
|
|
4b36fc540d | ||
|
|
7cafbde5b1 | ||
|
|
7106e915ef | ||
|
|
f7c8ad6f38 | ||
|
|
8dd4a9fce1 | ||
|
|
52c4395b68 | ||
|
|
49fbae4fad | ||
|
|
2d91f1ab38 | ||
|
|
4d844548c2 | ||
|
|
29e1090d2c | ||
|
|
98f0655da4 | ||
|
|
b241d6d148 | ||
|
|
b7d9e41b43 | ||
|
|
3f000b9d54 | ||
|
|
4672884255 | ||
|
|
a0ae7ff139 | ||
|
|
8ac369b925 | ||
|
|
117f01e71f | ||
|
|
38416fbebb | ||
|
|
39a0350e08 | ||
|
|
a099d2a25a | ||
|
|
bd885da179 | ||
|
|
35db337f79 | ||
|
|
c3f1e0e06e | ||
|
|
b64ab276fb | ||
|
|
66610fb3e7 | ||
|
|
09dcdfa318 | ||
|
|
c574bbcb96 | ||
|
|
61deed70ad | ||
|
|
601747ac49 | ||
|
|
2afce31e95 | ||
|
|
ba08602e20 | ||
|
|
19dcf171e6 | ||
|
|
1bbd685a23 | ||
|
|
02c3bc75f3 | ||
|
|
951467f8ca | ||
|
|
cdfea145d9 | ||
|
|
431ad2940b | ||
|
|
72b8ac2a50 | ||
|
|
d3153e9bb4 | ||
|
|
5045d66c1c | ||
|
|
e71bbd4122 | ||
|
|
c4160e1d50 | ||
|
|
46245ab4aa | ||
|
|
8880b5ff4f | ||
|
|
2c0d42e0da | ||
|
|
1d70a23859 | ||
|
|
0335bc26ca | ||
|
|
60166ac008 | ||
|
|
f760df1e34 | ||
|
|
6fe6342ca4 | ||
|
|
175f2702d1 | ||
|
|
a7db713b1e | ||
|
|
1a0c9cd4e6 | ||
|
|
742e56fc5f | ||
|
|
0262d94dd3 | ||
|
|
32d036c2b0 | ||
|
|
1a63d32fe3 | ||
|
|
ec9b00e255 | ||
|
|
d7cb549eac | ||
|
|
168157bfe1 | ||
|
|
7cbb7718eb | ||
|
|
2d65265fc6 | ||
|
|
30fe9f5236 | ||
|
|
49821062fb | ||
|
|
99439be04f | ||
|
|
17ccac92ee | ||
|
|
dd4c243d7c | ||
|
|
34031bfd6b | ||
|
|
a7a030fedd | ||
|
|
4c50551249 | ||
|
|
483ca9677c | ||
|
|
f00a2e003d | ||
|
|
0b878216ed | ||
|
|
f49a92e742 | ||
|
|
ee218a5fed | ||
|
|
a9196ef149 | ||
|
|
04bf37b52a | ||
|
|
5a3c11f619 | ||
|
|
c89d43d269 | ||
|
|
12f33176bf | ||
|
|
eb779e5bf6 | ||
|
|
47ace5d654 | ||
|
|
70db8c69d2 | ||
|
|
962d16172c | ||
|
|
5f9e675d00 | ||
|
|
4cdc7f33ce | ||
|
|
c359d1e264 | ||
|
|
0c42d039ff | ||
|
|
55503c89e9 | ||
|
|
b8cb6f4246 | ||
|
|
d25e23623a | ||
|
|
a143797ac9 | ||
|
|
09de12c02a | ||
|
|
246e8770ac | ||
|
|
ea059790a9 | ||
|
|
6ae4907520 | ||
|
|
5f768444ac | ||
|
|
93481a98db | ||
|
|
0ebb7c4f9a | ||
|
|
652ea98a2b | ||
|
|
877383679c | ||
|
|
e041a49ad7 | ||
|
|
dc06426ada | ||
|
|
1478ac18fa | ||
|
|
558309599c | ||
|
|
40d1eb37dc | ||
|
|
b8d6c5d007 | ||
|
|
85a9da0e8a | ||
|
|
faa0275700 | ||
|
|
eea56ea0e5 | ||
|
|
ab313aacd8 | ||
|
|
a88ee0725d | ||
|
|
e57c6a9d2e | ||
|
|
f863f4b7ae | ||
|
|
4bbae38150 | ||
|
|
31807b674b | ||
|
|
75fbd9e615 | ||
|
|
42ec624a91 | ||
|
|
b4c13069c0 | ||
|
|
6b1b33481c | ||
|
|
1471e0a247 | ||
|
|
a04bb100bf | ||
|
|
f9b1d3059a | ||
|
|
bddb689e86 | ||
|
|
757f7beeaf | ||
|
|
6582ee1624 | ||
|
|
04e7073aca | ||
|
|
2ccd072416 | ||
|
|
79ecbc83ae | ||
|
|
2a6051ae35 | ||
|
|
cf8e366ae1 | ||
|
|
d42197db3a | ||
|
|
8017b1b479 | ||
|
|
375a243131 | ||
|
|
70e6904eb7 | ||
|
|
42745c52df | ||
|
|
8479989ea8 | ||
|
|
ee7675f7ae | ||
|
|
37b91c40b6 | ||
|
|
4027a87e7f | ||
|
|
84992746de | ||
|
|
4397819b44 | ||
|
|
ee443361b9 | ||
|
|
e5c1071073 | ||
|
|
8d941dc028 | ||
|
|
11f6939b85 | ||
|
|
1c3b66c1a8 | ||
|
|
a99f024dfc | ||
|
|
d58ba82388 | ||
|
|
738c6353ef | ||
|
|
dc547de592 | ||
|
|
adf20f188e | ||
|
|
b9b965753b | ||
|
|
036873e129 | ||
|
|
9d36dc9e39 | ||
|
|
ce02d430ba | ||
|
|
4abf2b06d8 | ||
|
|
2d27e3fee0 | ||
|
|
67771105ae | ||
|
|
de9ce6dd19 | ||
|
|
71ef4b079a | ||
|
|
f0d901e136 | ||
|
|
d3dd9d94d3 | ||
|
|
2938f7e506 | ||
|
|
1f5fa9a9bd | ||
|
|
96ac65b346 | ||
|
|
c00e4c24a3 | ||
|
|
844fe2c250 | ||
|
|
6da97c5403 | ||
|
|
1e41b79611 | ||
|
|
5d33493233 | ||
|
|
38c65dbea8 | ||
|
|
218ba0d189 | ||
|
|
a36294a529 | ||
|
|
333ee713d4 | ||
|
|
b8ae0ed565 | ||
|
|
3b07198b8a | ||
|
|
8b94833ea0 | ||
|
|
0862ffef98 | ||
|
|
95dc12a71b | ||
|
|
0f29745bfa | ||
|
|
012950cb68 | ||
|
|
f9c06c22ae | ||
|
|
83c2704d53 | ||
|
|
74b6b4dea2 | ||
|
|
f6bc03324f | ||
|
|
3d4443e1b0 | ||
|
|
366a5d5737 | ||
|
|
6d5d279f61 | ||
|
|
20970db404 | ||
|
|
1eff4ab4ff | ||
|
|
1aed3b4d91 | ||
|
|
9379bc1f6e | ||
|
|
9ec2b9ff1d | ||
|
|
4bfe326868 | ||
|
|
6074bb033d | ||
|
|
ec7b543bf2 | ||
|
|
b7c3cc5532 | ||
|
|
1c3d642be2 | ||
|
|
a2f84e943a | ||
|
|
4ae36d319c | ||
|
|
7fe0bb21b2 | ||
|
|
0174da6c77 | ||
|
|
3d6d714495 | ||
|
|
adc3703fba | ||
|
|
6de253df58 | ||
|
|
f31bea2c70 | ||
|
|
b61286891f | ||
|
|
6db7642331 | ||
|
|
91b5d039cc | ||
|
|
5692e9dc86 | ||
|
|
7849ff2b92 | ||
|
|
e77ec102d9 | ||
|
|
7ea23e0fcc | ||
|
|
e60a3a697e | ||
|
|
d899360c34 | ||
|
|
98c4d9bdba | ||
|
|
24730ebd0f | ||
|
|
cab346686d | ||
|
|
d369d4b50f | ||
|
|
6897d063c2 | ||
|
|
4ddddc134a | ||
|
|
44a330ac02 | ||
|
|
f220ea367e | ||
|
|
43c0770770 | ||
|
|
82286ea7ed | ||
|
|
01d6f751b1 | ||
|
|
7a25f348e4 | ||
|
|
12a0a35adf | ||
|
|
0f8abb6026 | ||
|
|
8e2f3fdecd | ||
|
|
26e3634814 | ||
|
|
9a897e0cf4 | ||
|
|
1bc15e57a6 | ||
|
|
09087a4d8f | ||
|
|
6934380b9c | ||
|
|
618aed3278 | ||
|
|
c8c0061244 | ||
|
|
ad4b68a390 | ||
|
|
a95e313733 | ||
|
|
8b25d3e32e | ||
|
|
a0a7623743 | ||
|
|
5e20ac0454 | ||
|
|
d182736bd7 | ||
|
|
159398b391 | ||
|
|
e4ccb1c43f | ||
|
|
9d75ad6cea | ||
|
|
b30697ea20 | ||
|
|
0c0f05b6d9 | ||
|
|
50d21fcdca | ||
|
|
c49a02d1c5 | ||
|
|
c26594a3ec | ||
|
|
6572e6f10e | ||
|
|
5144907ec0 | ||
|
|
ba8ed23292 | ||
|
|
6583b0f530 | ||
|
|
a3bea76994 | ||
|
|
ebe8ce7109 | ||
|
|
df9d879f22 | ||
|
|
46b1aca5f1 | ||
|
|
dd4ac4e6d6 | ||
|
|
fe23e61036 | ||
|
|
b88c4211b2 | ||
|
|
9e0d1a7285 | ||
|
|
73f94fecb5 | ||
|
|
0e24ebdb26 | ||
|
|
d5bd2143e2 | ||
|
|
7d412e4c18 | ||
|
|
ac5ac5e06b | ||
|
|
53e4dc7acb | ||
|
|
4d3e7d694b | ||
|
|
ca271900e6 | ||
|
|
f8c8c3deff | ||
|
|
76ad67307b | ||
|
|
ffab7ae697 | ||
|
|
8909386d0c | ||
|
|
8e9f22537f | ||
|
|
9366003f7b | ||
|
|
72a18d6abf | ||
|
|
79e81340dc | ||
|
|
4baffed481 | ||
|
|
77131a39d4 | ||
|
|
40d00f7cbd | ||
|
|
fe1f825fdf | ||
|
|
50d2bfb272 | ||
|
|
06eedff49f | ||
|
|
afbbad1604 | ||
|
|
9d5c4dbb33 | ||
|
|
ce90472624 | ||
|
|
85dcc8ba63 | ||
|
|
c9e192fae4 | ||
|
|
cee8b2d787 | ||
|
|
d3465be672 | ||
|
|
e99b33c51e | ||
|
|
43fe985143 | ||
|
|
699046dad5 | ||
|
|
6482a5e214 | ||
|
|
fef3879c85 | ||
|
|
9f7b066ca6 | ||
|
|
02218e624a | ||
|
|
5a5928483f | ||
|
|
11b3cee346 | ||
|
|
7a94724dbc | ||
|
|
79c79432f5 | ||
|
|
d32ea9f9a1 | ||
|
|
907c2c7e97 | ||
|
|
960e45d0fa | ||
|
|
8bf9103fcf | ||
|
|
4eb78e151e | ||
|
|
3efd810fe1 | ||
|
|
c21cdc2131 | ||
|
|
44c69ded78 | ||
|
|
1e170714c0 | ||
|
|
1387e29591 | ||
|
|
10d96a9767 | ||
|
|
fd5041965b | ||
|
|
33c624c76f | ||
|
|
913f93b91f | ||
|
|
c1facb939e | ||
|
|
ad16d8b532 | ||
|
|
9f0620f97e | ||
|
|
9e52e6a320 | ||
|
|
f3a0c390b1 | ||
|
|
bb32c0480c | ||
|
|
8d167baf46 | ||
|
|
fdcaa358e5 | ||
|
|
5d16836b05 | ||
|
|
fc22677f3f | ||
|
|
e979c2753b |
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
logs/*
|
||||
data/*
|
||||
.github
|
||||
tmp/*
|
||||
django.db
|
||||
celerybeat.pid
|
||||
16
.github/ISSUE_TEMPLATE.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[简述你的问题]
|
||||
|
||||
##### 使用版本
|
||||
[请提供你使用的Jumpserver版本 0.3.2 或 0.5.0]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
||||
##### 具体表现[截图可能会更好些,最好能截全]
|
||||
|
||||
|
||||
##### 其他
|
||||
|
||||
|
||||
[注:] 完成后请关闭 issue
|
||||
71
.gitignore
vendored
71
.gitignore
vendored
@@ -1,43 +1,34 @@
|
||||
*.py[cod]
|
||||
.idea
|
||||
test.py
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
.DS_Store
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
.env
|
||||
env
|
||||
env*
|
||||
venv
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
__pycache__
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
*.egg
|
||||
*.egg-info
|
||||
_mailinglist
|
||||
dump.rdb
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
node_modules
|
||||
logs
|
||||
keys
|
||||
jumpserver.conf
|
||||
nohup.out
|
||||
.cache/
|
||||
.idea/
|
||||
db.sqlite3
|
||||
config.py
|
||||
migrations/
|
||||
*.log
|
||||
host_rsa_key
|
||||
*.bat
|
||||
tags
|
||||
jumpserver.iml
|
||||
.python-version
|
||||
tmp/*
|
||||
sessions/*
|
||||
media
|
||||
celerybeat.pid
|
||||
django.db
|
||||
celerybeat-schedule.db
|
||||
data/static
|
||||
docs/_build/
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,4 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
@@ -336,4 +336,4 @@ This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
Public License instead of this License.
|
||||
75
README.md
75
README.md
@@ -1,34 +1,61 @@
|
||||
#欢迎使用Jumpserver
|
||||
**Jumpserver**是一款由python编写开源的跳板机(堡垒机)系统,实现了跳板机应有的功能
|
||||
## Jumpserver
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
|
||||
|
||||
----
|
||||
|
||||
> **统计管理** 统一管理用户
|
||||
>
|
||||
> **授权** 授权用户登录特定主机
|
||||
>
|
||||
> **审计** 审计用户操作
|
||||
>
|
||||
> **web管理** 漂亮的web管理界面
|
||||
Jumpserver是全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
|
||||
|
||||
## 主要模块
|
||||
#### 用户管理 ####
|
||||
负责用户管理,添加用户,编辑用户,建立部门,建立用户组等
|
||||
#### 资产管理 ####
|
||||
负责资产管理,添加资产,编辑资产,建立IDC,建立用户组等
|
||||
#### 授权管理 ####
|
||||
负责授权用户登录某些特定主机,授权sudo,查看授权申请
|
||||
#### 日志审计 ####
|
||||
负责用户操作的审计,监控用户操作,统计用户操作记录,中断用户操作
|
||||
#### 上传下载 ####
|
||||
负责用户文件上传下载
|
||||
Jumpserver使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
|
||||
|
||||
[官网](http://www.jumpserver.org)
|
||||
Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发限制。
|
||||
|
||||
[demo站点](http://demo.jumpserver.org)
|
||||
改变世界,从一点点开始。
|
||||
|
||||
[更新log](http://laoguang.blog.51cto.com/6013350/1635853)
|
||||
----
|
||||
|
||||
[部署文档](http://laoguang.blog.51cto.com/6013350/1636273)
|
||||
### 功能
|
||||
- 统一认证
|
||||
- 资产管理
|
||||
- 统一授权
|
||||
- 审计
|
||||
- 支持LDAP认证
|
||||
- Web terminal
|
||||
- SSH Server
|
||||
- 支持Windows RDP
|
||||
|
||||
### 开始使用
|
||||
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html)
|
||||
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html)
|
||||
|
||||
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
|
||||
|
||||
### Demo 和 截图
|
||||
|
||||
我们提供了DEMO和截图可以让你快速了解Jumpserver
|
||||
|
||||
[DEMO](http://demo.jumpserver.org)
|
||||
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
|
||||
我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互,
|
||||
|
||||
- [python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
|
||||
- [java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
|
||||
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2018 Beijing Duizhan Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
5
apps/__init__.py
Normal file
5
apps/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.2.0"
|
||||
6
apps/assets/api/__init__.py
Normal file
6
apps/assets/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .admin_user import *
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .system_user import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
83
apps/assets/api/admin_user.py
Normal file
83
apps/assets/api/admin_user.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.db import transaction
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsSuperUser
|
||||
from ..models import AdminUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import test_admin_user_connectability_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
|
||||
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
|
||||
]
|
||||
|
||||
|
||||
class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
"""
|
||||
Admin user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
queryset = AdminUser.objects.all()
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
|
||||
class AdminUserAuthApi(generics.UpdateAPIView):
|
||||
queryset = AdminUser.objects.all()
|
||||
serializer_class = serializers.AdminUserAuthSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
|
||||
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
queryset = AdminUser.objects.all()
|
||||
serializer_class = serializers.ReplaceNodeAdminUserSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
nodes = serializer.validated_data['nodes']
|
||||
assets = []
|
||||
for node in nodes:
|
||||
assets.extend([asset.id for asset in node.get_all_assets()])
|
||||
|
||||
with transaction.atomic():
|
||||
Asset.objects.filter(id__in=assets).update(admin_user=admin_user)
|
||||
|
||||
return Response({"msg": "ok"})
|
||||
else:
|
||||
return Response({'error': serializer.errors}, status=400)
|
||||
|
||||
|
||||
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
"""
|
||||
queryset = AdminUser.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
task = test_admin_user_connectability_manual.delay(admin_user)
|
||||
return Response({"task": task.id})
|
||||
107
apps/assets/api/asset.py
Normal file
107
apps/assets/api/asset.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser, \
|
||||
NodePermissionUtil
|
||||
from ..models import Asset, SystemUser, AdminUser, Node
|
||||
from .. import serializers
|
||||
from ..tasks import update_asset_hardware_info_manual, \
|
||||
test_asset_connectability_manual
|
||||
from ..utils import LabelFilter
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'UserAssetListView', 'AssetListUpdateApi',
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi'
|
||||
]
|
||||
|
||||
|
||||
class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
filter_fields = ("hostname", "ip")
|
||||
search_fields = filter_fields
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
admin_user_id = self.request.query_params.get('admin_user_id')
|
||||
node_id = self.request.query_params.get("node_id")
|
||||
|
||||
if admin_user_id:
|
||||
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
|
||||
queryset = queryset.filter(admin_user=admin_user)
|
||||
if node_id:
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
if not node.is_root():
|
||||
queryset = queryset.filter(
|
||||
nodes__key__regex='{}(:[0-9]+)*$'.format(node.key),
|
||||
).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class UserAssetListView(generics.ListAPIView):
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
assets_granted = NodePermissionUtil.get_user_assets(self.request.user).keys()
|
||||
queryset = self.queryset.filter(
|
||||
id__in=[asset.id for asset in assets_granted]
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
|
||||
"""
|
||||
Asset bulk update api
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
|
||||
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Refresh asset hardware info
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = update_asset_hardware_info_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = test_asset_connectability_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
55
apps/assets/api/domain.py
Normal file
55
apps/assets/api/domain.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsSuperUser, IsSuperUserOrAppUser
|
||||
from ..models import Domain, Gateway
|
||||
from ..utils import test_gateway_connectability
|
||||
from .. import serializers
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
|
||||
|
||||
|
||||
class DomainViewSet(BulkModelViewSet):
|
||||
queryset = Domain.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.query_params.get('gateway'):
|
||||
return serializers.DomainWithGatewaySerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.query_params.get('gateway'):
|
||||
self.permission_classes = (IsSuperUserOrAppUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class GatewayViewSet(BulkModelViewSet):
|
||||
filter_fields = ("domain",)
|
||||
search_fields = filter_fields
|
||||
queryset = Gateway.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
model = Gateway
|
||||
object = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
ok, e = test_gateway_connectability(self.object)
|
||||
if ok:
|
||||
return Response("ok")
|
||||
else:
|
||||
return Response({"failed": e}, status=404)
|
||||
38
apps/assets/api/label.py
Normal file
38
apps/assets/api/label.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.db.models import Count
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsSuperUser
|
||||
from ..models import Label
|
||||
from .. import serializers
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['LabelViewSet']
|
||||
|
||||
|
||||
class LabelViewSet(BulkModelViewSet):
|
||||
queryset = Label.objects.annotate(asset_count=Count("assets"))
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if request.query_params.get("distinct"):
|
||||
self.serializer_class = serializers.LabelDistinctSerializer
|
||||
self.queryset = self.queryset.values("name").distinct()
|
||||
return super().list(request, *args, **kwargs)
|
||||
231
apps/assets/api/node.py
Normal file
231
apps/assets/api/node.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from rest_framework import generics, mixins
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from ..hands import IsSuperUser
|
||||
from ..models import Node
|
||||
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util
|
||||
from .. import serializers
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi',
|
||||
'NodeAssetsApi', 'NodeWithAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi',
|
||||
'NodeReplaceAssetsApi',
|
||||
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
|
||||
'TestNodeConnectiveApi'
|
||||
]
|
||||
|
||||
|
||||
class NodeViewSet(BulkModelViewSet):
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
child_key = Node.root().get_next_child_key()
|
||||
serializer.validated_data["key"] = child_key
|
||||
serializer.save()
|
||||
|
||||
|
||||
class NodeWithAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializers = serializers.NodeSerializer
|
||||
|
||||
def get_node(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('node')
|
||||
if not pk:
|
||||
node = Node.root()
|
||||
else:
|
||||
node = get_object_or_404(Node, pk)
|
||||
return node
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
node = self.get_node()
|
||||
children = node.get_children()
|
||||
assets = node.get_assets()
|
||||
queryset.extend(list(children))
|
||||
|
||||
for asset in assets:
|
||||
node = Node()
|
||||
node.id = asset.id
|
||||
node.parent = node.id
|
||||
node.value = asset.hostname
|
||||
queryset.append(node)
|
||||
return queryset
|
||||
|
||||
|
||||
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not request.data.get("value"):
|
||||
request.data["value"] = _("New node {}").format(
|
||||
Node.root().get_next_child_key().split(":")[-1]
|
||||
)
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
value = request.data.get("value")
|
||||
node = instance.create_child(value=value)
|
||||
return Response(
|
||||
{"id": node.id, "key": node.key, "value": node.value},
|
||||
status=201,
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
if not pk:
|
||||
node = Node.root()
|
||||
else:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
return node
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
query_all = self.request.query_params.get("all")
|
||||
query_assets = self.request.query_params.get('assets')
|
||||
node = self.get_object()
|
||||
if node == Node.root():
|
||||
queryset.append(node)
|
||||
if query_all:
|
||||
children = node.get_all_children()
|
||||
else:
|
||||
children = node.get_children()
|
||||
|
||||
queryset.extend(list(children))
|
||||
if query_assets:
|
||||
assets = node.get_assets()
|
||||
for asset in assets:
|
||||
node_fake = Node()
|
||||
node_fake.id = asset.id
|
||||
node_fake.parent = node
|
||||
node_fake.value = asset.hostname
|
||||
node_fake.is_asset = True
|
||||
queryset.append(node_fake)
|
||||
return queryset
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
node_id = self.kwargs.get('pk')
|
||||
query_all = self.request.query_params.get('all')
|
||||
instance = get_object_or_404(Node, pk=node_id)
|
||||
if query_all:
|
||||
return instance.get_all_assets()
|
||||
else:
|
||||
return instance.get_assets()
|
||||
|
||||
|
||||
class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.NodeAddChildrenSerializer
|
||||
instance = None
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
nodes_id = request.data.get("nodes")
|
||||
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
|
||||
for node in children:
|
||||
if not node:
|
||||
continue
|
||||
node.parent = instance
|
||||
node.save()
|
||||
return Response("OK")
|
||||
|
||||
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
instance.assets.add(*tuple(assets))
|
||||
|
||||
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
if instance != Node.root():
|
||||
instance.assets.remove(*tuple(assets))
|
||||
|
||||
|
||||
class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
instance = None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
for asset in assets:
|
||||
asset.nodes.set([instance])
|
||||
|
||||
|
||||
class RefreshNodeHardwareInfoApi(APIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
model = Node
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
task_name = _("更新节点资产硬件信息: {}".format(node.name))
|
||||
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class TestNodeConnectiveApi(APIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
model = Node
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
task_name = _("测试节点下资产是否可连接: {}".format(node.name))
|
||||
task = test_asset_connectability_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
75
apps/assets/api/system_user.py
Normal file
75
apps/assets/api/system_user.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsSuperUser, IsSuperUserOrAppUser
|
||||
from ..models import SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import push_system_user_to_assets_manual, \
|
||||
test_system_user_connectability_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi'
|
||||
]
|
||||
|
||||
|
||||
class SystemUserViewSet(BulkModelViewSet):
|
||||
"""
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
|
||||
|
||||
class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
Get system user auth info
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
|
||||
|
||||
class SystemUserPushApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
task = push_system_user_to_assets_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
task = test_system_user_connectability_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
11
apps/assets/apps.py
Normal file
11
apps/assets/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
name = 'assets'
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
super().ready()
|
||||
38
apps/assets/const.py
Normal file
38
apps/assets/const.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
UPDATE_ASSETS_HARDWARE_TASKS = [
|
||||
{
|
||||
'name': "setup",
|
||||
'action': {
|
||||
'module': 'setup'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ADMIN_USER_CONN_CACHE_KEY = "ADMIN_USER_CONN_{}"
|
||||
TEST_ADMIN_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}"
|
||||
|
||||
SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}"
|
||||
TEST_SYSTEM_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
TASK_OPTIONS = {
|
||||
'timeout': 10,
|
||||
'forks': 10,
|
||||
}
|
||||
6
apps/assets/forms/__init__.py
Normal file
6
apps/assets/forms/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .user import *
|
||||
from .domain import *
|
||||
152
apps/assets/forms/asset.py
Normal file
152
apps/assets/forms/asset.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import Asset, AdminUser
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm']
|
||||
|
||||
|
||||
class AssetCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'hostname', 'ip', 'public_ip', 'port', 'comment',
|
||||
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
|
||||
'domain',
|
||||
|
||||
]
|
||||
widgets = {
|
||||
'nodes': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Nodes')
|
||||
}),
|
||||
'admin_user': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Admin user')
|
||||
}),
|
||||
'labels': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Label')
|
||||
}),
|
||||
'port': forms.TextInput(),
|
||||
'domain': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Domain')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'admin_user': _(
|
||||
'root or other NOPASSWD sudo privilege user existed in asset,'
|
||||
'If asset is windows or other set any one, more see admin user left menu'
|
||||
),
|
||||
'platform': _("* required Must set exact system platform, Windows, Linux ..."),
|
||||
'domain': _("If your have some network not connect with each other, you can set domain")
|
||||
}
|
||||
|
||||
|
||||
class AssetUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
|
||||
'public_ip', 'number', 'comment', 'admin_user', 'labels',
|
||||
'domain',
|
||||
]
|
||||
widgets = {
|
||||
'nodes': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Node')
|
||||
}),
|
||||
'admin_user': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Admin user')
|
||||
}),
|
||||
'labels': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Label')
|
||||
}),
|
||||
'port': forms.TextInput(),
|
||||
'domain': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Domain')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'cluster': '* required',
|
||||
'admin_user': _(
|
||||
'root or other NOPASSWD sudo privilege user existed in asset,'
|
||||
'If asset is windows or other set any one, more see admin user left menu'
|
||||
),
|
||||
'platform': _("* required Must set exact system platform, Windows, Linux ..."),
|
||||
'domain': _("If your have some network not connect with each other, you can set domain")
|
||||
}
|
||||
|
||||
|
||||
class AssetBulkUpdateForm(forms.ModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
required=True, help_text='* required',
|
||||
label=_('Select assets'), queryset=Asset.objects.all(),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Select assets')
|
||||
}
|
||||
)
|
||||
)
|
||||
port = forms.IntegerField(
|
||||
label=_('Port'), required=False, min_value=1, max_value=65535,
|
||||
)
|
||||
admin_user = forms.ModelChoiceField(
|
||||
required=False, queryset=AdminUser.objects.all(),
|
||||
label=_("Admin user"),
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Admin user')
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'assets', 'port', 'admin_user', 'labels', 'nodes', 'platform'
|
||||
]
|
||||
widgets = {
|
||||
'labels': forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Label')}
|
||||
),
|
||||
'nodes': forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Node')}
|
||||
),
|
||||
}
|
||||
|
||||
def save(self, commit=True):
|
||||
changed_fields = []
|
||||
for field in self._meta.fields:
|
||||
if self.data.get(field) not in [None, '']:
|
||||
changed_fields.append(field)
|
||||
|
||||
cleaned_data = {k: v for k, v in self.cleaned_data.items()
|
||||
if k in changed_fields}
|
||||
assets = cleaned_data.pop('assets')
|
||||
labels = cleaned_data.pop('labels', [])
|
||||
nodes = cleaned_data.pop('nodes')
|
||||
assets = Asset.objects.filter(id__in=[asset.id for asset in assets])
|
||||
assets.update(**cleaned_data)
|
||||
|
||||
if labels:
|
||||
for label in labels:
|
||||
label.assets.add(*tuple(assets))
|
||||
if nodes:
|
||||
for node in nodes:
|
||||
node.assets.add(*tuple(assets))
|
||||
return assets
|
||||
60
apps/assets/forms/domain.py
Normal file
60
apps/assets/forms/domain.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import Domain, Asset, Gateway
|
||||
from .user import PasswordAndKeyAuthForm
|
||||
|
||||
__all__ = ['DomainForm', 'GatewayForm']
|
||||
|
||||
|
||||
class DomainForm(forms.ModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
queryset=Asset.objects.all(), label=_('Asset'), required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = ['name', 'comment', 'assets']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get('instance', None):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
instance.assets.set(assets)
|
||||
return instance
|
||||
|
||||
|
||||
class GatewayForm(PasswordAndKeyAuthForm):
|
||||
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
instance = super().save()
|
||||
password = self.cleaned_data.get('password')
|
||||
private_key, public_key = super().gen_keys()
|
||||
instance.set_auth(password=password, private_key=private_key)
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = [
|
||||
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
|
||||
'private_key_file', 'is_active', 'comment',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
33
apps/assets/forms/label.py
Normal file
33
apps/assets/forms/label.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import Label, Asset
|
||||
|
||||
__all__ = ['LabelForm']
|
||||
|
||||
|
||||
class LabelForm(forms.ModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
queryset=Asset.objects.all(), label=_('Asset'), required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ['name', 'value', 'assets']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get('instance', None):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, commit=True):
|
||||
label = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
label.assets.set(assets)
|
||||
return label
|
||||
128
apps/assets/forms/user.py
Normal file
128
apps/assets/forms/user.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import AdminUser, SystemUser
|
||||
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm',
|
||||
]
|
||||
|
||||
|
||||
class FileForm(forms.Form):
|
||||
file = forms.FileField()
|
||||
|
||||
|
||||
class PasswordAndKeyAuthForm(forms.ModelForm):
|
||||
# Form field name can not start with `_`, so redefine it,
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput, max_length=128,
|
||||
strip=True, required=False,
|
||||
help_text=_('Password or private key passphrase'),
|
||||
label=_("Password"),
|
||||
)
|
||||
# Need use upload private key file except paste private key content
|
||||
private_key_file = forms.FileField(required=False, label=_("Private key"))
|
||||
|
||||
def clean_private_key_file(self):
|
||||
private_key_file = self.cleaned_data['private_key_file']
|
||||
password = self.cleaned_data['password']
|
||||
|
||||
if private_key_file:
|
||||
key_string = private_key_file.read()
|
||||
private_key_file.seek(0)
|
||||
if not validate_ssh_private_key(key_string, password):
|
||||
raise forms.ValidationError(_('Invalid private key'))
|
||||
return private_key_file
|
||||
|
||||
def validate_password_key(self):
|
||||
password = self.cleaned_data['password']
|
||||
private_key_file = self.cleaned_data.get('private_key_file', '')
|
||||
|
||||
if not password and not private_key_file:
|
||||
raise forms.ValidationError(_(
|
||||
'Password and private key file must be input one'
|
||||
))
|
||||
|
||||
def gen_keys(self):
|
||||
password = self.cleaned_data.get('password', '') or None
|
||||
private_key_file = self.cleaned_data['private_key_file']
|
||||
public_key = private_key = None
|
||||
|
||||
if private_key_file:
|
||||
private_key = private_key_file.read().strip().decode('utf-8')
|
||||
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
|
||||
return private_key, public_key
|
||||
|
||||
|
||||
class AdminUserForm(PasswordAndKeyAuthForm):
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
admin_user = super().save(commit=commit)
|
||||
password = self.cleaned_data.get('password', '') or None
|
||||
private_key, public_key = super().gen_keys()
|
||||
admin_user.set_auth(password=password, public_key=public_key, private_key=private_key)
|
||||
return admin_user
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.instance:
|
||||
super().validate_password_key()
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['name', 'username', 'password', 'private_key_file', 'comment']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
|
||||
|
||||
class SystemUserForm(PasswordAndKeyAuthForm):
|
||||
# Admin user assets define, let user select, save it in form not in view
|
||||
auto_generate_key = forms.BooleanField(initial=True, required=False)
|
||||
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
system_user = super().save()
|
||||
password = self.cleaned_data.get('password', '') or None
|
||||
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
|
||||
private_key, public_key = super().gen_keys()
|
||||
|
||||
if auto_generate_key:
|
||||
logger.info('Auto generate key and set system user auth')
|
||||
system_user.auto_gen_auth()
|
||||
else:
|
||||
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
|
||||
return system_user
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
auto_generate = self.cleaned_data.get('auto_generate_key')
|
||||
if not self.instance and not auto_generate:
|
||||
super().validate_password_key()
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'name', 'username', 'protocol', 'auto_generate_key',
|
||||
'password', 'private_key_file', 'auto_push', 'sudo',
|
||||
'comment', 'shell', 'priority',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
'auto_push': _('Auto push system user to asset'),
|
||||
'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'),
|
||||
}
|
||||
17
apps/assets/hands.py
Normal file
17
apps/assets/hands.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
jumpserver.__app__.hands.py
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This app depends other apps api, function .. should be import or write mack here.
|
||||
|
||||
Other module of this app shouldn't connect with other app.
|
||||
|
||||
:copyright: (c) 2014-2018 by Jumpserver Team.
|
||||
:license: GPL v2, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
from common.mixins import AdminUserRequiredMixin
|
||||
from common.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser
|
||||
from users.models import User, UserGroup
|
||||
from perms.utils import NodePermissionUtil
|
||||
168
apps/assets/migrations/0001_initial.py
Normal file
168
apps/assets/migrations/0001_initial.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-12-21 16:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import assets.models.utils
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
def add_default_group(apps, schema_editor):
|
||||
group_model = apps.get_model("assets", "AssetGroup")
|
||||
db_alias = schema_editor.connection.alias
|
||||
group_model.objects.using(db_alias).create(
|
||||
name="Default"
|
||||
)
|
||||
|
||||
|
||||
def add_default_cluster(apps, schema_editor):
|
||||
cluster_model = apps.get_model("assets", "Cluster")
|
||||
db_alias = schema_editor.connection.alias
|
||||
cluster_model.objects.using(db_alias).create(
|
||||
name="Default"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AdminUser',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('username', models.CharField(max_length=16, verbose_name='Username')),
|
||||
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
|
||||
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.CharField(max_length=32, null=True, verbose_name='Created by')),
|
||||
('become', models.BooleanField(default=True)),
|
||||
('become_method', models.CharField(choices=[('sudo', 'sudo'), ('su', 'su')], default='sudo', max_length=4)),
|
||||
('become_user', models.CharField(default='root', max_length=64)),
|
||||
('_become_pass', models.CharField(default='', max_length=128)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Asset',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
|
||||
('hostname', models.CharField(max_length=128, unique=True, verbose_name='Hostname')),
|
||||
('port', models.IntegerField(default=22, verbose_name='Port')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('type', models.CharField(blank=True, choices=[('Server', 'Server'), ('VM', 'VM'), ('Switch', 'Switch'), ('Router', 'Router'), ('Firewall', 'Firewall'), ('Storage', 'Storage')], default='Server', max_length=16, null=True, verbose_name='Asset type')),
|
||||
('env', models.CharField(blank=True, choices=[('Prod', 'Production'), ('Dev', 'Development'), ('Test', 'Testing')], default='Prod', max_length=8, null=True, verbose_name='Asset environment')),
|
||||
('status', models.CharField(blank=True, choices=[('In use', 'In use'), ('Out of use', 'Out of use')], default='In use', max_length=12, null=True, verbose_name='Asset status')),
|
||||
('public_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Public IP')),
|
||||
('remote_card_ip', models.CharField(blank=True, max_length=16, null=True, verbose_name='Remote control card IP')),
|
||||
('cabinet_no', models.CharField(blank=True, max_length=32, null=True, verbose_name='Cabinet number')),
|
||||
('cabinet_pos', models.IntegerField(blank=True, null=True, verbose_name='Cabinet position')),
|
||||
('number', models.CharField(blank=True, max_length=32, null=True, verbose_name='Asset number')),
|
||||
('vendor', models.CharField(blank=True, max_length=64, null=True, verbose_name='Vendor')),
|
||||
('model', models.CharField(blank=True, max_length=54, null=True, verbose_name='Model')),
|
||||
('sn', models.CharField(blank=True, max_length=128, null=True, verbose_name='Serial number')),
|
||||
('cpu_model', models.CharField(blank=True, max_length=64, null=True, verbose_name='CPU model')),
|
||||
('cpu_count', models.IntegerField(null=True, verbose_name='CPU count')),
|
||||
('cpu_cores', models.IntegerField(null=True, verbose_name='CPU cores')),
|
||||
('memory', models.CharField(blank=True, max_length=64, null=True, verbose_name='Memory')),
|
||||
('disk_total', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Disk total')),
|
||||
('disk_info', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Disk info')),
|
||||
('platform', models.CharField(blank=True, max_length=128, null=True, verbose_name='Platform')),
|
||||
('os', models.CharField(blank=True, max_length=128, null=True, verbose_name='OS')),
|
||||
('os_version', models.CharField(blank=True, max_length=16, null=True, verbose_name='OS version')),
|
||||
('os_arch', models.CharField(blank=True, max_length=16, null=True, verbose_name='OS arch')),
|
||||
('hostname_raw', models.CharField(blank=True, max_length=128, null=True, verbose_name='Hostname raw')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
('admin_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.AdminUser', verbose_name='Admin user')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetGroup',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64, unique=True, verbose_name='Name')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Cluster',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=32, verbose_name='Name')),
|
||||
('bandwidth', models.CharField(blank=True, max_length=32, verbose_name='Bandwidth')),
|
||||
('contact', models.CharField(blank=True, max_length=128, verbose_name='Contact')),
|
||||
('phone', models.CharField(blank=True, max_length=32, verbose_name='Phone')),
|
||||
('address', models.CharField(blank=True, max_length=128, verbose_name='Address')),
|
||||
('intranet', models.TextField(blank=True, verbose_name='Intranet')),
|
||||
('extranet', models.TextField(blank=True, verbose_name='Extranet')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('operator', models.CharField(blank=True, max_length=32, verbose_name='Operator')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('admin_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.AdminUser', verbose_name='Admin user')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SystemUser',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('username', models.CharField(max_length=16, verbose_name='Username')),
|
||||
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
|
||||
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.CharField(max_length=32, null=True, verbose_name='Created by')),
|
||||
('priority', models.IntegerField(default=10, verbose_name='Priority')),
|
||||
('protocol', models.CharField(choices=[('ssh', 'ssh')], default='ssh', max_length=16, verbose_name='Protocol')),
|
||||
('auto_push', models.BooleanField(default=True, verbose_name='Auto push')),
|
||||
('sudo', models.TextField(default='/sbin/ifconfig', verbose_name='Sudo')),
|
||||
('shell', models.CharField(default='/bin/bash', max_length=64, verbose_name='Shell')),
|
||||
('cluster', models.ManyToManyField(blank=True, to='assets.Cluster', verbose_name='Cluster')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.AssetGroup', verbose_name='Asset groups'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='asset',
|
||||
unique_together=set([('ip', 'port')]),
|
||||
),
|
||||
|
||||
migrations.RunPython(add_default_cluster),
|
||||
migrations.RunPython(add_default_group),
|
||||
]
|
||||
11
apps/assets/models/__init__.py
Normal file
11
apps/assets/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .user import AdminUser, SystemUser
|
||||
from .label import Label
|
||||
from .cluster import *
|
||||
from .group import *
|
||||
from .domain import *
|
||||
from .node import *
|
||||
from .asset import *
|
||||
from .utils import *
|
||||
190
apps/assets/models/asset.py
Normal file
190
apps/assets/models/asset.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import random
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
|
||||
from .user import AdminUser, SystemUser
|
||||
|
||||
__all__ = ['Asset']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def default_cluster():
|
||||
from .cluster import Cluster
|
||||
name = "Default"
|
||||
defaults = {"name": name}
|
||||
cluster, created = Cluster.objects.get_or_create(
|
||||
defaults=defaults, name=name
|
||||
)
|
||||
return cluster.id
|
||||
|
||||
|
||||
def default_node():
|
||||
try:
|
||||
from .node import Node
|
||||
return Node.root()
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class Asset(models.Model):
|
||||
# Important
|
||||
PLATFORM_CHOICES = (
|
||||
('Linux', 'Linux'),
|
||||
('Unix', 'Unix'),
|
||||
('MacOS', 'MacOS'),
|
||||
('BSD', 'BSD'),
|
||||
('Windows', 'Windows'),
|
||||
('Other', 'Other'),
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname'))
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
|
||||
# Auth
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
|
||||
|
||||
# Some information
|
||||
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
|
||||
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
|
||||
# Collect
|
||||
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
|
||||
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
|
||||
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
|
||||
|
||||
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
|
||||
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
|
||||
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
|
||||
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
|
||||
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
|
||||
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
|
||||
|
||||
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
|
||||
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
|
||||
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
|
||||
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
|
||||
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
warning = ''
|
||||
if not self.is_active:
|
||||
warning += ' inactive'
|
||||
else:
|
||||
return True, ''
|
||||
return False, warning
|
||||
|
||||
def is_unixlike(self):
|
||||
if self.platform not in ("Windows",):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_nodes(self):
|
||||
from .node import Node
|
||||
return self.nodes.all() or [Node.root()]
|
||||
|
||||
@property
|
||||
def hardware_info(self):
|
||||
if self.cpu_count:
|
||||
return '{} Core {} {}'.format(
|
||||
self.cpu_count * self.cpu_cores,
|
||||
self.memory, self.disk_total
|
||||
)
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def is_connective(self):
|
||||
if not self.is_unixlike():
|
||||
return True
|
||||
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
|
||||
if val == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def to_json(self):
|
||||
info = {
|
||||
'id': self.id,
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
'port': self.port,
|
||||
}
|
||||
if self.domain and self.domain.gateway_set.all():
|
||||
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
|
||||
return info
|
||||
|
||||
def get_auth_info(self):
|
||||
if self.admin_user:
|
||||
return {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
'private_key': self.admin_user.private_key_file,
|
||||
'become': self.admin_user.become_info,
|
||||
}
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""
|
||||
Ansible use it create inventory, First using asset user,
|
||||
otherwise using cluster admin user
|
||||
|
||||
Todo: May be move to ops implements it
|
||||
"""
|
||||
data = self.to_json()
|
||||
if self.admin_user:
|
||||
admin_user = self.admin_user
|
||||
data.update({
|
||||
'username': admin_user.username,
|
||||
'password': admin_user.password,
|
||||
'private_key': admin_user.private_key_file,
|
||||
'become': admin_user.become_info,
|
||||
'groups': [node.value for node in self.nodes.all()],
|
||||
})
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
unique_together = ('ip', 'port')
|
||||
verbose_name = _("Asset")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
asset = cls(ip='%s.%s.%s.%s' % (i, i, i, i),
|
||||
hostname=forgery_py.internet.user_name(True),
|
||||
admin_user=choice(AdminUser.objects.all()),
|
||||
port=22,
|
||||
created_by='Fake')
|
||||
try:
|
||||
asset.save()
|
||||
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
|
||||
logger.debug('Generate fake asset : %s' % asset.ip)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
126
apps/assets/models/base.py
Normal file
126
apps/assets/models/base.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
import uuid
|
||||
from hashlib import md5
|
||||
|
||||
import sshpubkeys
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen
|
||||
from .utils import private_key_validator
|
||||
|
||||
signer = get_signer()
|
||||
|
||||
|
||||
class AssetUser(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=128, verbose_name=_('Username'))
|
||||
_password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
_private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ])
|
||||
_public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
if self._password:
|
||||
return signer.unsign(self._password)
|
||||
else:
|
||||
return None
|
||||
|
||||
@password.setter
|
||||
def password(self, password_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._password = signer.sign(password_raw)
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
if self._private_key:
|
||||
return signer.unsign(self._private_key)
|
||||
|
||||
@private_key.setter
|
||||
def private_key(self, private_key_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._private_key = signer.sign(private_key_raw)
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
if self._private_key:
|
||||
key_str = signer.unsign(self._private_key)
|
||||
return ssh_key_string_to_obj(key_str, password=self.password)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def private_key_file(self):
|
||||
if not self.private_key_obj:
|
||||
return None
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
key_str = signer.unsign(self._private_key)
|
||||
key_name = '.' + md5(key_str.encode('utf-8')).hexdigest()
|
||||
key_path = os.path.join(tmp_dir, key_name)
|
||||
if not os.path.exists(key_path):
|
||||
self.private_key_obj.write_private_key_file(key_path)
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
key = signer.unsign(self._public_key)
|
||||
if key:
|
||||
return key
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def public_key_obj(self):
|
||||
if self.public_key:
|
||||
try:
|
||||
return sshpubkeys.SSHKey(self.public_key)
|
||||
except TabError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def set_auth(self, password=None, private_key=None, public_key=None):
|
||||
update_fields = []
|
||||
if password:
|
||||
self._password = signer.sign(password)
|
||||
update_fields.append('_password')
|
||||
if private_key:
|
||||
self._private_key = signer.sign(private_key)
|
||||
update_fields.append('_private_key')
|
||||
if public_key:
|
||||
self._public_key = signer.sign(public_key)
|
||||
update_fields.append('_public_key')
|
||||
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def auto_gen_auth(self):
|
||||
password = str(uuid.uuid4())
|
||||
private_key, public_key = ssh_key_gen(
|
||||
username=self.name, password=password
|
||||
)
|
||||
self.set_auth(password=password,
|
||||
private_key=private_key,
|
||||
public_key=public_key)
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""Push system user use it"""
|
||||
return {
|
||||
'name': self.name,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'public_key': self.public_key,
|
||||
'private_key': self.private_key_file,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
63
apps/assets/models/cluster.py
Normal file
63
apps/assets/models/cluster.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['Cluster']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cluster(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=32, verbose_name=_('Name'))
|
||||
admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user"))
|
||||
bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth'))
|
||||
contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact'))
|
||||
phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone'))
|
||||
address = models.CharField(max_length=128, blank=True, verbose_name=_("Address"))
|
||||
intranet = models.TextField(blank=True, verbose_name=_('Intranet'))
|
||||
extranet = models.TextField(blank=True, verbose_name=_('Extranet'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))
|
||||
operator = models.CharField(max_length=32, blank=True, verbose_name=_('Operator'))
|
||||
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def initial(cls):
|
||||
return cls.objects.get_or_create(name=_('Default'), created_by=_('System'), comment=_('Default Cluster'))[0]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("Cluster")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=5):
|
||||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
cluster = cls(name=forgery_py.name.full_name(),
|
||||
bandwidth='200M',
|
||||
contact=forgery_py.name.full_name(),
|
||||
phone=forgery_py.address.phone(),
|
||||
address=forgery_py.address.city() + forgery_py.address.street_address(),
|
||||
operator=choice(['北京联通', '北京电信', 'BGP全网通']),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
cluster.save()
|
||||
logger.debug('Generate fake asset group: %s' % cluster.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
52
apps/assets/models/domain.py
Normal file
52
apps/assets/models/domain.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import uuid
|
||||
import random
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .base import AssetUser
|
||||
|
||||
__all__ = ['Domain', 'Gateway']
|
||||
|
||||
|
||||
class Domain(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
||||
verbose_name=_('Date created'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def has_gateway(self):
|
||||
return self.gateway_set.filter(is_active=True).exists()
|
||||
|
||||
@property
|
||||
def gateways(self):
|
||||
return self.gateway_set.filter(is_active=True)
|
||||
|
||||
def random_gateway(self):
|
||||
return random.choice(self.gateways)
|
||||
|
||||
|
||||
class Gateway(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=SSH_PROTOCOL, verbose_name=_("Protocol"))
|
||||
domain = models.ForeignKey(Domain, verbose_name=_("Domain"))
|
||||
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
53
apps/assets/models/group.py
Normal file
53
apps/assets/models/group.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
import logging
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['AssetGroup']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssetGroup(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("Asset group")
|
||||
|
||||
@classmethod
|
||||
def initial(cls):
|
||||
asset_group = cls(name=_('Default'), comment=_('Default asset group'))
|
||||
asset_group.save()
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
group = cls(name=forgery_py.name.full_name(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
group.save()
|
||||
logger.debug('Generate fake asset group: %s' % group.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
37
apps/assets/models/label.py
Normal file
37
apps/assets/models/label.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Label(models.Model):
|
||||
SYSTEM_CATEGORY = "S"
|
||||
USER_CATEGORY = "U"
|
||||
CATEGORY_CHOICES = (
|
||||
("S", _("System")),
|
||||
("U", _("User"))
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
value = models.CharField(max_length=128, verbose_name=_("Value"))
|
||||
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, default=USER_CATEGORY, verbose_name=_("Category"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(
|
||||
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_queryset_group_by_name(cls):
|
||||
names = cls.objects.values_list('name', flat=True)
|
||||
for name in names:
|
||||
yield name, cls.objects.filter(name=name)
|
||||
|
||||
def __str__(self):
|
||||
return "{}:{}".format(self.name, self.value)
|
||||
|
||||
class Meta:
|
||||
db_table = "assets_label"
|
||||
unique_together = ('name', 'value')
|
||||
124
apps/assets/models/node.py
Normal file
124
apps/assets/models/node.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ['Node']
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
|
||||
value = models.CharField(max_length=128, unique=True, verbose_name=_("Value"))
|
||||
child_mark = models.IntegerField(default=0)
|
||||
date_create = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
is_asset = False
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.value
|
||||
|
||||
@property
|
||||
def full_value(self):
|
||||
if self == self.__class__.root():
|
||||
return self.value
|
||||
else:
|
||||
return '{} / {}'.format(self.parent.full_value, self.value)
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return len(self.key.split(':'))
|
||||
|
||||
def get_next_child_key(self):
|
||||
mark = self.child_mark
|
||||
self.child_mark += 1
|
||||
self.save()
|
||||
return "{}:{}".format(self.key, mark)
|
||||
|
||||
def create_child(self, value):
|
||||
child_key = self.get_next_child_key()
|
||||
child = self.__class__.objects.create(key=child_key, value=value)
|
||||
return child
|
||||
|
||||
def get_children(self):
|
||||
return self.__class__.objects.filter(key__regex=r'{}:[0-9]+$'.format(self.key))
|
||||
|
||||
def get_all_children(self):
|
||||
return self.__class__.objects.filter(key__startswith='{}:'.format(self.key))
|
||||
|
||||
def get_family(self):
|
||||
children = list(self.get_all_children())
|
||||
children.append(self)
|
||||
return children
|
||||
|
||||
def get_assets(self):
|
||||
from .asset import Asset
|
||||
assets = Asset.objects.filter(nodes__id=self.id)
|
||||
return assets
|
||||
|
||||
def get_active_assets(self):
|
||||
return self.get_assets().filter(is_active=True)
|
||||
|
||||
def get_all_assets(self):
|
||||
from .asset import Asset
|
||||
if self.is_root():
|
||||
assets = Asset.objects.all()
|
||||
else:
|
||||
nodes = self.get_family()
|
||||
assets = Asset.objects.filter(nodes__in=nodes).distinct()
|
||||
return assets
|
||||
|
||||
def has_assets(self):
|
||||
return self.get_all_assets()
|
||||
|
||||
def get_all_active_assets(self):
|
||||
return self.get_all_assets().filter(is_active=True)
|
||||
|
||||
def is_root(self):
|
||||
return self.key == '0'
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
if self.key == "0":
|
||||
return self.__class__.root()
|
||||
elif not self.key.startswith("0"):
|
||||
return self.__class__.root()
|
||||
|
||||
parent_key = ":".join(self.key.split(":")[:-1])
|
||||
try:
|
||||
parent = self.__class__.objects.get(key=parent_key)
|
||||
except Node.DoesNotExist:
|
||||
return self.__class__.root()
|
||||
else:
|
||||
return parent
|
||||
|
||||
@parent.setter
|
||||
def parent(self, parent):
|
||||
self.key = parent.get_next_child_key()
|
||||
|
||||
@property
|
||||
def ancestor(self):
|
||||
if self.parent == self.__class__.root():
|
||||
return [self.__class__.root()]
|
||||
else:
|
||||
return [self.parent, *tuple(self.parent.ancestor)]
|
||||
|
||||
@property
|
||||
def ancestor_with_node(self):
|
||||
ancestor = self.ancestor
|
||||
ancestor.insert(0, self)
|
||||
return ancestor
|
||||
|
||||
@classmethod
|
||||
def root(cls):
|
||||
obj, created = cls.objects.get_or_create(
|
||||
key='0', defaults={"key": '0', 'value': "ROOT"}
|
||||
)
|
||||
return obj
|
||||
169
apps/assets/models/user.py
Normal file
169
apps/assets/models/user.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_signer
|
||||
from ..const import SYSTEM_USER_CONN_CACHE_KEY
|
||||
from .base import AssetUser
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser',]
|
||||
logger = logging.getLogger(__name__)
|
||||
signer = get_signer()
|
||||
|
||||
|
||||
class AdminUser(AssetUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
"""
|
||||
BECOME_METHOD_CHOICES = (
|
||||
('sudo', 'sudo'),
|
||||
('su', 'su'),
|
||||
)
|
||||
become = models.BooleanField(default=True)
|
||||
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
|
||||
become_user = models.CharField(default='root', max_length=64)
|
||||
_become_pass = models.CharField(default='', max_length=128)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def become_pass(self):
|
||||
password = signer.unsign(self._become_pass)
|
||||
if password:
|
||||
return password
|
||||
else:
|
||||
return ""
|
||||
|
||||
@become_pass.setter
|
||||
def become_pass(self, password):
|
||||
self._become_pass = signer.sign(password)
|
||||
|
||||
@property
|
||||
def become_info(self):
|
||||
if self.become:
|
||||
info = {
|
||||
"method": self.become_method,
|
||||
"user": self.become_user,
|
||||
"pass": self.become_pass,
|
||||
}
|
||||
else:
|
||||
info = None
|
||||
return info
|
||||
|
||||
def get_related_assets(self):
|
||||
assets = self.asset_set.all()
|
||||
return assets
|
||||
|
||||
@property
|
||||
def assets_amount(self):
|
||||
return self.get_related_assets().count()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("Admin user")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=10):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
obj = cls(name=forgery_py.name.full_name(),
|
||||
username=forgery_py.internet.user_name(),
|
||||
password=forgery_py.lorem_ipsum.word(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
obj.save()
|
||||
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
|
||||
class SystemUser(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
)
|
||||
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
priority = models.IntegerField(default=10, verbose_name=_("Priority"))
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'username': self.username,
|
||||
'protocol': self.protocol,
|
||||
'priority': self.priority,
|
||||
'auto_push': self.auto_push,
|
||||
}
|
||||
|
||||
def get_assets(self):
|
||||
assets = set(self.assets.all())
|
||||
return assets
|
||||
|
||||
@property
|
||||
def assets_connective(self):
|
||||
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
|
||||
return _result
|
||||
|
||||
@property
|
||||
def unreachable_assets(self):
|
||||
return list(self.assets_connective.get('dark', {}).keys())
|
||||
|
||||
@property
|
||||
def reachable_assets(self):
|
||||
return self.assets_connective.get('contacted', [])
|
||||
|
||||
def is_need_push(self):
|
||||
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("System user")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=10):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
obj = cls(name=forgery_py.name.full_name(),
|
||||
username=forgery_py.internet.user_name(),
|
||||
password=forgery_py.lorem_ipsum.word(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
obj.save()
|
||||
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
35
apps/assets/models/utils.py
Normal file
35
apps/assets/models/utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from common.utils import validate_ssh_private_key
|
||||
|
||||
|
||||
__all__ = ['init_model', 'generate_fake']
|
||||
|
||||
|
||||
def init_model():
|
||||
from . import SystemUser, AdminUser, Asset
|
||||
for cls in [SystemUser, AdminUser, Asset]:
|
||||
if hasattr(cls, 'initial'):
|
||||
cls.initial()
|
||||
|
||||
|
||||
def generate_fake():
|
||||
from . import SystemUser, AdminUser, Asset
|
||||
for cls in [SystemUser, AdminUser, Asset]:
|
||||
if hasattr(cls, 'generate_fake'):
|
||||
cls.generate_fake()
|
||||
|
||||
|
||||
def private_key_validator(value):
|
||||
if not validate_ssh_private_key(value):
|
||||
raise ValidationError(
|
||||
_('%(value)s is not an even number'),
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
9
apps/assets/serializers/__init__.py
Normal file
9
apps/assets/serializers/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .asset import *
|
||||
from .admin_user import *
|
||||
from .label import *
|
||||
from .system_user import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
69
apps/assets/serializers/admin_user.py
Normal file
69
apps/assets/serializers/admin_user.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.core.cache import cache
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Node, AdminUser
|
||||
from ..const import ADMIN_USER_CONN_CACHE_KEY
|
||||
|
||||
from .base import AuthSerializer
|
||||
|
||||
|
||||
class AdminUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
管理用户
|
||||
"""
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
unreachable_amount = serializers.SerializerMethodField()
|
||||
reachable_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = '__all__'
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
return [f for f in fields if not f.startswith('_')]
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_amount(obj):
|
||||
data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name))
|
||||
if data:
|
||||
return len(data.get('dark'))
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_reachable_amount(obj):
|
||||
data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name))
|
||||
if data:
|
||||
return len(data.get('contacted'))
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return obj.assets_amount
|
||||
|
||||
|
||||
class AdminUserAuthSerializer(AuthSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['password', 'private_key']
|
||||
|
||||
|
||||
class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
管理用户更新关联到的集群
|
||||
"""
|
||||
nodes = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Node.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['id', 'nodes']
|
||||
|
||||
|
||||
|
||||
62
apps/assets/serializers/asset.py
Normal file
62
apps/assets/serializers/asset.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from ..models import Asset
|
||||
from .system_user import AssetSystemUserSerializer
|
||||
|
||||
|
||||
class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
资产的数据结构
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
list_serializer_class = BulkListSerializer
|
||||
fields = '__all__'
|
||||
validators = [] # If not set to [], partial bulk update will be error
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'hardware_info', 'is_connective',
|
||||
])
|
||||
return fields
|
||||
|
||||
|
||||
class AssetGrantedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
被授权资产的数据结构
|
||||
"""
|
||||
system_users_granted = AssetSystemUserSerializer(many=True, read_only=True)
|
||||
system_users_join = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = (
|
||||
"id", "hostname", "ip", "port", "system_users_granted",
|
||||
"is_active", "system_users_join", "os", 'domain',
|
||||
"platform", "comment"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_system_users_join(obj):
|
||||
system_users = [s.username for s in obj.system_users_granted]
|
||||
return ', '.join(system_users)
|
||||
|
||||
|
||||
class MyAssetGrantedSerializer(AssetGrantedSerializer):
|
||||
"""
|
||||
普通用户获取授权的资产定义的数据结构
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = (
|
||||
"id", "hostname", "system_users_granted",
|
||||
"is_active", "system_users_join",
|
||||
"os", "platform", "comment",
|
||||
)
|
||||
26
apps/assets/serializers/base.py
Normal file
26
apps/assets/serializers/base.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from common.utils import ssh_pubkey_gen
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
return None, None
|
||||
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
|
||||
return private_key, public_key
|
||||
|
||||
def save(self, **kwargs):
|
||||
password = self.validated_data.pop('password', None) or None
|
||||
private_key = self.validated_data.pop('private_key', None) or None
|
||||
self.instance = super().save(**kwargs)
|
||||
if password or private_key:
|
||||
private_key, public_key = self.gen_keys(private_key, password)
|
||||
self.instance.set_auth(password=password, private_key=private_key,
|
||||
public_key=public_key)
|
||||
return self.instance
|
||||
50
apps/assets/serializers/domain.py
Normal file
50
apps/assets/serializers/domain.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Domain, Gateway
|
||||
|
||||
|
||||
class DomainSerializer(serializers.ModelSerializer):
|
||||
asset_count = serializers.SerializerMethodField()
|
||||
gateway_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
return obj.assets.count()
|
||||
|
||||
@staticmethod
|
||||
def get_gateway_count(obj):
|
||||
return obj.gateway_set.all().count()
|
||||
|
||||
|
||||
class GatewaySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = [
|
||||
'id', 'name', 'ip', 'port', 'protocol', 'username',
|
||||
'domain', 'is_active', 'date_created', 'date_updated',
|
||||
'created_by', 'comment',
|
||||
]
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(
|
||||
['password', 'private_key']
|
||||
)
|
||||
return fields
|
||||
|
||||
|
||||
class DomainWithGatewaySerializer(serializers.ModelSerializer):
|
||||
gateways = GatewayWithAuthSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = '__all__'
|
||||
37
apps/assets/serializers/label.py
Normal file
37
apps/assets/serializers/label.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from ..models import Label
|
||||
|
||||
|
||||
class LabelSerializer(serializers.ModelSerializer):
|
||||
asset_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = '__all__'
|
||||
list_serializer_class = BulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
return obj.assets.count()
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['get_category_display'])
|
||||
return fields
|
||||
|
||||
|
||||
class LabelDistinctSerializer(serializers.ModelSerializer):
|
||||
value = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ("name", "value")
|
||||
|
||||
@staticmethod
|
||||
def get_value(obj):
|
||||
labels = Label.objects.filter(name=obj["name"])
|
||||
return ', '.join([label.value for label in labels])
|
||||
72
apps/assets/serializers/node.py
Normal file
72
apps/assets/serializers/node.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from ..models import Asset, Node
|
||||
from .asset import AssetGrantedSerializer
|
||||
|
||||
|
||||
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
授权资产组
|
||||
"""
|
||||
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
parent = serializers.SerializerMethodField()
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = [
|
||||
'id', 'key', 'name', 'value', 'parent',
|
||||
'assets_granted', 'assets_amount',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return len(obj.assets_granted)
|
||||
|
||||
@staticmethod
|
||||
def get_name(obj):
|
||||
return obj.name
|
||||
|
||||
@staticmethod
|
||||
def get_parent(obj):
|
||||
return obj.parent.id
|
||||
|
||||
|
||||
class NodeSerializer(serializers.ModelSerializer):
|
||||
parent = serializers.SerializerMethodField()
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_asset']
|
||||
list_serializer_class = BulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_parent(obj):
|
||||
return obj.parent.id
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return obj.get_all_assets().count()
|
||||
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
field = fields["key"]
|
||||
field.required = False
|
||||
return fields
|
||||
|
||||
|
||||
class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = ['assets']
|
||||
|
||||
|
||||
class NodeAddChildrenSerializer(serializers.Serializer):
|
||||
nodes = serializers.ListField()
|
||||
68
apps/assets/serializers/system_user.py
Normal file
68
apps/assets/serializers/system_user.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import SystemUser
|
||||
from .base import AuthSerializer
|
||||
|
||||
|
||||
class SystemUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
系统用户
|
||||
"""
|
||||
unreachable_amount = serializers.SerializerMethodField()
|
||||
reachable_amount = serializers.SerializerMethodField()
|
||||
unreachable_assets = serializers.SerializerMethodField()
|
||||
reachable_assets = serializers.SerializerMethodField()
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
exclude = ('_password', '_private_key', '_public_key')
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_assets(obj):
|
||||
return obj.unreachable_assets
|
||||
|
||||
@staticmethod
|
||||
def get_reachable_assets(obj):
|
||||
return obj.reachable_assets
|
||||
|
||||
def get_unreachable_amount(self, obj):
|
||||
return len(self.get_unreachable_assets(obj))
|
||||
|
||||
def get_reachable_amount(self, obj):
|
||||
return len(self.get_reachable_assets(obj))
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return len(obj.get_assets())
|
||||
|
||||
|
||||
class SystemUserAuthSerializer(AuthSerializer):
|
||||
"""
|
||||
系统用户认证信息
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
"id", "name", "username", "protocol",
|
||||
"password", "private_key",
|
||||
]
|
||||
|
||||
|
||||
class AssetSystemUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少
|
||||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',)
|
||||
|
||||
|
||||
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
系统用户最基本信息的数据结构
|
||||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username')
|
||||
5
apps/assets/signals.py
Normal file
5
apps/assets/signals.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.dispatch import Signal
|
||||
|
||||
on_app_ready = Signal()
|
||||
84
apps/assets/signals_handler.py
Normal file
84
apps/assets/signals_handler.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.utils import get_logger
|
||||
from .models import Asset, SystemUser, Node
|
||||
from .tasks import update_assets_hardware_info_util, \
|
||||
test_asset_connectability_util, push_system_user_to_assets
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def update_asset_hardware_info_on_created(asset):
|
||||
logger.debug("Update asset `{}` hardware info".format(asset))
|
||||
update_assets_hardware_info_util.delay([asset])
|
||||
|
||||
|
||||
def test_asset_conn_on_created(asset):
|
||||
logger.debug("Test asset `{}` connectability".format(asset))
|
||||
test_asset_connectability_util.delay([asset])
|
||||
|
||||
|
||||
def set_asset_root_node(asset):
|
||||
logger.debug("Set asset default node: {}".format(Node.root()))
|
||||
asset.nodes.add(Node.root())
|
||||
|
||||
|
||||
@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier")
|
||||
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
logger.info("Asset `{}` create signal received".format(instance))
|
||||
update_asset_hardware_info_on_created(instance)
|
||||
test_asset_conn_on_created(instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
|
||||
def on_system_user_update(sender, instance=None, created=True, **kwargs):
|
||||
if instance and not created:
|
||||
logger.info("System user `{}` update signal received".format(instance))
|
||||
assets = instance.assets.all()
|
||||
push_system_user_to_assets.delay(instance, assets)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.nodes.through)
|
||||
def on_system_user_nodes_change(sender, instance=None, **kwargs):
|
||||
if instance and kwargs["action"] == "post_add":
|
||||
assets = set()
|
||||
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
for node in nodes:
|
||||
assets.update(set(node.get_all_assets()))
|
||||
instance.assets.add(*tuple(assets))
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.assets.through)
|
||||
def on_system_user_assets_change(sender, instance=None, **kwargs):
|
||||
if instance and kwargs["action"] == "post_add":
|
||||
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
push_system_user_to_assets(instance, assets)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_node_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Asset) and kwargs['action'] == 'post_add':
|
||||
logger.debug("Asset node change signal received")
|
||||
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
system_users_assets = defaultdict(set)
|
||||
system_users = SystemUser.objects.filter(nodes__in=nodes)
|
||||
for system_user in system_users:
|
||||
system_users_assets[system_user].update({instance})
|
||||
for system_user, assets in system_users_assets.items():
|
||||
system_user.assets.add(*tuple(assets))
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_node_assets_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Node) and kwargs['action'] == 'post_add':
|
||||
logger.debug("Node assets change signal received")
|
||||
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
system_users = SystemUser.objects.filter(nodes=instance)
|
||||
for system_user in system_users:
|
||||
system_user.assets.add(*tuple(assets))
|
||||
415
apps/assets/tasks.py
Normal file
415
apps/assets/tasks.py
Normal file
@@ -0,0 +1,415 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
from celery import shared_task
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_object_or_none, capacity_convert, \
|
||||
sum_capacity, encrypt_password, get_logger
|
||||
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
|
||||
after_app_ready_start
|
||||
from ops.celery import app as celery_app
|
||||
|
||||
from .models import SystemUser, AdminUser, Asset
|
||||
from . import const
|
||||
|
||||
|
||||
FORKS = 10
|
||||
TIMEOUT = 60
|
||||
logger = get_logger(__file__)
|
||||
CACHE_MAX_TIME = 60*60*60
|
||||
disk_pattern = re.compile(r'^hd|sd|xvd|vd')
|
||||
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
|
||||
|
||||
|
||||
@shared_task
|
||||
def set_assets_hardware_info(result, **kwargs):
|
||||
"""
|
||||
Using ops task run result, to update asset info
|
||||
|
||||
@shared_task must be exit, because we using it as a task callback, is must
|
||||
be a celery task also
|
||||
:param result:
|
||||
:param kwargs: {task_name: ""}
|
||||
:return:
|
||||
"""
|
||||
result_raw = result[0]
|
||||
assets_updated = []
|
||||
for hostname, info in result_raw.get('ok', {}).items():
|
||||
info = info.get('setup', {}).get('ansible_facts', {})
|
||||
if not info:
|
||||
logger.error("Get asset info failed: {}".format(hostname))
|
||||
continue
|
||||
|
||||
asset = get_object_or_none(Asset, hostname=hostname)
|
||||
if not asset:
|
||||
continue
|
||||
|
||||
___vendor = info.get('ansible_system_vendor', 'Unknown')
|
||||
___model = info.get('ansible_product_name', 'Unknown')
|
||||
___sn = info.get('ansible_product_serial', 'Unknown')
|
||||
|
||||
for ___cpu_model in info.get('ansible_processor', []):
|
||||
if ___cpu_model.endswith('GHz') or ___cpu_model.startswith("Intel"):
|
||||
break
|
||||
else:
|
||||
___cpu_model = 'Unknown'
|
||||
___cpu_model = ___cpu_model[:64]
|
||||
___cpu_count = info.get('ansible_processor_count', 0)
|
||||
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', []))
|
||||
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb')))
|
||||
disk_info = {}
|
||||
for dev, dev_info in info.get('ansible_devices', {}).items():
|
||||
if disk_pattern.match(dev) and dev_info['removable'] == '0':
|
||||
disk_info[dev] = dev_info['size']
|
||||
___disk_total = '%s %s' % sum_capacity(disk_info.values())
|
||||
___disk_info = json.dumps(disk_info)
|
||||
|
||||
___platform = info.get('ansible_system', 'Unknown')
|
||||
___os = info.get('ansible_distribution', 'Unknown')
|
||||
___os_version = info.get('ansible_distribution_version', 'Unknown')
|
||||
___os_arch = info.get('ansible_architecture', 'Unknown')
|
||||
___hostname_raw = info.get('ansible_hostname', 'Unknown')
|
||||
|
||||
for k, v in locals().items():
|
||||
if k.startswith('___'):
|
||||
setattr(asset, k.strip('_'), v)
|
||||
asset.save()
|
||||
assets_updated.append(asset)
|
||||
return assets_updated
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_assets_hardware_info_util(assets, task_name=None):
|
||||
"""
|
||||
Using ansible api to update asset hardware info
|
||||
:param assets: asset seq
|
||||
:param task_name: task_name running
|
||||
:return: result summary ['contacted': {}, 'dark': {}]
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
if task_name is None:
|
||||
# task_name = _("Update some assets hardware info")
|
||||
task_name = _("更新资产硬件信息")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hostname_list:
|
||||
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
)
|
||||
result = task.run()
|
||||
# Todo: may be somewhere using
|
||||
# Manual run callback function
|
||||
set_assets_hardware_info(result)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
# task_name = _("Update asset hardware info")
|
||||
task_name = _("更新资产硬件信息")
|
||||
return update_assets_hardware_info_util([asset], task_name=task_name)
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def update_assets_hardware_info_period():
|
||||
"""
|
||||
Update asset hardware period task
|
||||
:return:
|
||||
"""
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, update assets hardware info pass")
|
||||
return
|
||||
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
# task_name = _("Update assets hardware info period")
|
||||
task_name = _("定期更新资产硬件信息")
|
||||
hostname_list = [
|
||||
asset.hostname for asset in Asset.objects.all()
|
||||
if asset.is_active and asset.is_unixlike()
|
||||
]
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
|
||||
# Only create, schedule by celery beat
|
||||
update_or_create_ansible_task(
|
||||
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
|
||||
)
|
||||
|
||||
|
||||
## ADMIN USER CONNECTIVE ##
|
||||
|
||||
@shared_task
|
||||
def set_admin_user_connectability_info(result, **kwargs):
|
||||
admin_user = kwargs.get("admin_user")
|
||||
task_name = kwargs.get("task_name")
|
||||
if admin_user is None and task_name is not None:
|
||||
admin_user = task_name.split(":")[-1]
|
||||
|
||||
raw, summary = result
|
||||
cache_key = const.ADMIN_USER_CONN_CACHE_KEY.format(admin_user)
|
||||
cache.set(cache_key, summary, CACHE_MAX_TIME)
|
||||
|
||||
for i in summary.get('contacted', []):
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME)
|
||||
|
||||
for i, msg in summary.get('dark', {}).items():
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME)
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = [asset.hostname for asset in assets
|
||||
if asset.is_active and asset.is_unixlike()]
|
||||
if not hosts:
|
||||
return
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
)
|
||||
result = task.run()
|
||||
set_admin_user_connectability_info(result, admin_user=admin_user.name)
|
||||
return result
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_admin_user_connectability_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test admin user connectability pass")
|
||||
return
|
||||
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
# task_name = _("Test admin user connectability period: {}".format(admin_user.name))
|
||||
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
|
||||
test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_manual(admin_user):
|
||||
# task_name = _("Test admin user connectability: {}").format(admin_user.name)
|
||||
task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
|
||||
return test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_connectability_util(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if task_name is None:
|
||||
# task_name = _("Test assets connectability")
|
||||
task_name = _("测试资产可连接性")
|
||||
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
return {}
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
)
|
||||
result = task.run()
|
||||
summary = result[1]
|
||||
for k in summary.get('dark'):
|
||||
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME)
|
||||
|
||||
for k in summary.get('contacted'):
|
||||
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME)
|
||||
return summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_connectability_manual(asset):
|
||||
summary = test_asset_connectability_util([asset])
|
||||
|
||||
if summary.get('dark'):
|
||||
return False, summary['dark']
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
|
||||
## System user connective ##
|
||||
|
||||
@shared_task
|
||||
def set_system_user_connectablity_info(result, **kwargs):
|
||||
summary = result[1]
|
||||
task_name = kwargs.get("task_name")
|
||||
system_user = kwargs.get("system_user")
|
||||
if system_user is None:
|
||||
system_user = task_name.split(":")[-1]
|
||||
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(system_user)
|
||||
cache.set(cache_key, summary, CACHE_MAX_TIME)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_util(system_user, task_name):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
:param system_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
assets = system_user.get_assets()
|
||||
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=system_user.name, created_by="System",
|
||||
)
|
||||
result = task.run()
|
||||
set_system_user_connectablity_info(result, system_user=system_user.name)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_manual(system_user):
|
||||
task_name = _("Test system user connectability: {}").format(system_user)
|
||||
return test_system_user_connectability_util(system_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_system_user_connectability_period():
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test system user connectability pass")
|
||||
return
|
||||
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
# task_name = _("Test system user connectability period: {}".format(system_user))
|
||||
task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
test_system_user_connectability_util(system_user, task_name)
|
||||
|
||||
|
||||
#### Push system user tasks ####
|
||||
|
||||
def get_push_system_user_tasks(system_user):
|
||||
# Set root as system user is dangerous
|
||||
if system_user.username == "root":
|
||||
return []
|
||||
|
||||
tasks = []
|
||||
if system_user.password:
|
||||
tasks.append({
|
||||
'name': 'Add user {}'.format(system_user.username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
system_user.username, system_user.shell,
|
||||
encrypt_password(system_user.password, salt="K3mIlKK"),
|
||||
),
|
||||
}
|
||||
})
|
||||
if system_user.public_key:
|
||||
tasks.append({
|
||||
'name': 'Set {} authorized key'.format(system_user.username),
|
||||
'action': {
|
||||
'module': 'authorized_key',
|
||||
'args': "user={} state=present key='{}'".format(
|
||||
system_user.username, system_user.public_key
|
||||
)
|
||||
}
|
||||
})
|
||||
if system_user.sudo:
|
||||
tasks.append({
|
||||
'name': 'Set {} sudo setting'.format(system_user.username),
|
||||
'action': {
|
||||
'module': 'lineinfile',
|
||||
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
|
||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
||||
"validate='visudo -cf %s'".format(
|
||||
system_user.username,
|
||||
system_user.sudo,
|
||||
)
|
||||
}
|
||||
})
|
||||
return tasks
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_util(system_users, assets, task_name):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
tasks = []
|
||||
for system_user in system_users:
|
||||
if not system_user.is_need_push():
|
||||
msg = "push system user `{}` passed, may be not auto push or ssh " \
|
||||
"protocol is not ssh".format(system_user.name)
|
||||
logger.info(msg)
|
||||
continue
|
||||
tasks.extend(get_push_system_user_tasks(system_user))
|
||||
|
||||
if not tasks:
|
||||
logger.info("Not tasks, passed")
|
||||
return {}
|
||||
|
||||
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hosts:
|
||||
logger.info("Not hosts, passed")
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System'
|
||||
)
|
||||
return task.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_assets()
|
||||
task_name = "推送系统用户到入资产: {}".format(system_user.name)
|
||||
return push_system_user_util([system_user], assets, task_name=task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets(system_user, assets):
|
||||
task_name = _("推送系统用户到入资产: {}").format(system_user.name)
|
||||
return push_system_user_util.delay([system_user], assets, task_name)
|
||||
|
||||
|
||||
# @shared_task
|
||||
# @register_as_period_task(interval=3600)
|
||||
# @after_app_ready_start
|
||||
# # @after_app_shutdown_clean
|
||||
# def push_system_user_period():
|
||||
# for system_user in SystemUser.objects.all():
|
||||
# push_system_user_related_nodes(system_user)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_group_bulk_update_modal{% endblock %}
|
||||
{% block modal_class %}modal-lg{% endblock %}
|
||||
{% block modal_title%}{% trans "Update asset group" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
{% load bootstrap3 %}
|
||||
<p class="text-success text-center">{% trans "Hint: only change the field you want to update." %}</p>
|
||||
<form method="post" class="form-horizontal" action="" id="fm_asset_group_bulk_update">
|
||||
<div class="form-group">
|
||||
<label for="assets" class="col-sm-2 control-label">{% trans 'Assets' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<select name="assets" id="select2_groups" data-placeholder="{% trans 'Select Asset' %}" class="select2 form-control m-b" multiple>
|
||||
{% for asset in assets %}
|
||||
<option value="{{ asset.id }}">{{ asset.ip }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system_users" class="col-sm-2 control-label">{% trans 'System users' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<select name="system_users" id="select2_groups" data-placeholder="{% trans 'Select System Users' %}" class="select2 form-control m-b" multiple>
|
||||
{% for system_user in system_users %}
|
||||
<option value="{{ system_user.id }}">{{ system_user.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-9 col-lg-9 col-sm-offset-2">
|
||||
<div class="checkbox checkbox-success">
|
||||
<input type="checkbox" name="enable_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-OTP' %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %}
|
||||
29
apps/assets/templates/assets/_asset_import_modal.html
Normal file
29
apps/assets/templates/assets/_asset_import_modal.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_import_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Import asset" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<form method="post" action="{% url 'assets:asset-import' %}" id="fm_asset_import" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_assets">{% trans "Template" %}</label>
|
||||
<a href="{% url 'assets:asset-export' %}" style="display: block">{% trans 'Download' %}</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_users">{% trans "Asset csv file" %}</label>
|
||||
<input id="id_assets" type="file" name="file" />
|
||||
<span class="help-block red-fonts">
|
||||
{% trans 'If set id, will use this id update asset existed' %}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<p class="text-success" id="id_created"></p>
|
||||
<p id="id_created_detail"></p>
|
||||
<p class="text-warning" id="id_updated"></p>
|
||||
<p id="id_updated_detail"></p>
|
||||
<p class="text-danger" id="id_failed"></p>
|
||||
<p id="id_failed_detail"></p>
|
||||
</p>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_import{% endblock %}
|
||||
125
apps/assets/templates/assets/_asset_list_modal.html
Normal file
125
apps/assets/templates/assets/_asset_list_modal.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block modal_class %}modal-lg{% endblock %}
|
||||
{% block modal_id %}asset_list_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Asset list" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<style>
|
||||
.inmodal .modal-header {
|
||||
padding: 10px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#assetTree2.ztree * {
|
||||
background-color: #f8fafb;
|
||||
}
|
||||
#assetTree2.ztree {
|
||||
background-color: #f8fafb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree2" class="ztree">
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9 animated fadeInRight" id="split-right">
|
||||
<div class="mail-box-header">
|
||||
<table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var zTree2, asset_table2 = 0;
|
||||
function initTable2() {
|
||||
var options = {
|
||||
ele: $('#asset_list_modal_table'),
|
||||
ajax_url: '{% url "api-assets:asset-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" }
|
||||
],
|
||||
pageLength: 10
|
||||
};
|
||||
asset_table2 = jumpserver.initServerSideDataTable(options);
|
||||
return asset_table2
|
||||
}
|
||||
|
||||
function onSelected2(event, treeNode) {
|
||||
var url = asset_table2.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
asset_table2.ajax.url(url);
|
||||
asset_table2.ajax.reload();
|
||||
}
|
||||
|
||||
|
||||
function initTree2() {
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onSelected: onSelected2
|
||||
}
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-assets:node-list' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["pId"] = value["parent"];
|
||||
value["open"] = true;
|
||||
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
|
||||
value['value'] = value['value'];
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
|
||||
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
initTable2();
|
||||
initTree2();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_button %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_modal_confirm{% endblock %}
|
||||
|
||||
|
||||
|
||||
121
apps/assets/templates/assets/_system_user.html
Normal file
121
apps/assets/templates/assets/_system_user.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.priority layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
<div class="auto-generate">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_generate_key}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-fields">
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_push}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<h3>{% trans 'Other' %}</h3>
|
||||
{% bootstrap_field form.sudo layout="horizontal" %}
|
||||
{% bootstrap_field form.shell layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var password_id = '#' + '{{ form.password.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
|
||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||
|
||||
var need_change_field = [auto_generate_key, private_key_id, sudo_id, shell_id] ;
|
||||
|
||||
function authFieldsDisplay() {
|
||||
if ($(auto_generate_key).prop('checked')) {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
} else {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id).attr('value') === 'rdp') {
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).addClass('hidden')
|
||||
});
|
||||
$(password_id).removeClass('hidden')
|
||||
} else {
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).removeClass('hidden')
|
||||
});
|
||||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
authFieldsDisplay();
|
||||
protocolChange();
|
||||
$(auto_generate_key).change(function () {
|
||||
authFieldsDisplay();
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
137
apps/assets/templates/assets/admin_user_assets.html
Normal file
137
apps/assets/templates/assets/admin_user_assets.html
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table table-striped table-bordered table-hover" id="asset_list_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Reachable' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
|
||||
columns: [
|
||||
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "port" }, {data: "is_connective" }],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
62
apps/assets/templates/assets/admin_user_create_update.html
Normal file
62
apps/assets/templates/assets/admin_user_create_update.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
171
apps/assets/templates/assets/admin_user_detail.html
Normal file
171
apps/assets/templates/assets/admin_user_detail.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:admin-user-update' pk=admin_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-admin-user">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ admin_user.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ admin_user.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Username' %}:</td>
|
||||
<td><b>{{ admin_user.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ admin_user.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ asset_group.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ admin_user.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Replace node assets admin user with this' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table group_edit" id="table-clusters">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.value }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-change-admin-user">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function replaceNodeAssetsAdminUser(nodes) {
|
||||
var the_url = "{% url 'api-assets:replace-nodes-admin-user' pk=admin_user.id %}";
|
||||
var body = {
|
||||
nodes: nodes
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#nodes_selected').val('');
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
$('#opt_' + index).remove();
|
||||
});
|
||||
// clear jumpserver.groups_selected
|
||||
jumpserver.nodes_selected = {};
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
|
||||
jumpserver.nodes_selected = {};
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.nodes_selected[data.id] = data.text;
|
||||
}).on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id]
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-delete-admin-user', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ admin_user.name }}";
|
||||
var uid = "{{ admin_user.id }}";
|
||||
var the_url = '{% url "api-assets:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'assets:admin-user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
.on('click', '#btn-change-admin-user', function () {
|
||||
if (Object.keys(jumpserver.nodes_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var nodes = [];
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
nodes.push(index);
|
||||
});
|
||||
replaceNodeAssetsAdminUser(nodes);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
112
apps/assets/templates/assets/admin_user_list.html
Normal file
112
apps/assets/templates/assets/admin_user_list.html
Normal file
@@ -0,0 +1,112 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block table_search %}
|
||||
{% endblock %}
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
管理用户是服务器的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
Windows或其它硬件可以随意设置一个
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url "assets:admin-user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create admin user" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="admin_user_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Unreachable' %}</th>
|
||||
<th class="text-center">{% trans 'Ratio' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var options = {
|
||||
ele: $('#admin_user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
|
||||
} else {
|
||||
innerHtml = "<span>" + cellData + "</span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
|
||||
} else {
|
||||
innerHtml = "<span>" + cellData + "</span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
var val = 0;
|
||||
var innerHtml = "";
|
||||
var total = rowData.assets_amount;
|
||||
var reachable = rowData.reachable_amount;
|
||||
if (total !== 0) {
|
||||
val = reachable/total * 100;
|
||||
}
|
||||
|
||||
if (val === 100) {
|
||||
innerHtml = "<span class='text-navy'>" + val + "% </span>";
|
||||
} else {
|
||||
var num = new Number(val);
|
||||
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
|
||||
}},
|
||||
{targets: 8, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:admin-user-list" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
|
||||
.on('click', '.btn_admin_user_delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $("#admin_user_list_table").DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
69
apps/assets/templates/assets/asset_bulk_update.html
Normal file
69
apps/assets/templates/assets/asset_bulk_update.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<div class="ydxbd" id="formlists" style="display: block;">
|
||||
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
|
||||
<div class="tagBtnList">
|
||||
<a class="label label-primary" id="change_all" value="1">全选</a>
|
||||
{% for field in form %}
|
||||
{% if field.name != 'assets' %}
|
||||
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="form-horizontal" id="add_form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
<div class="form-group abc">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
}).on('click', '.field-tag', function() {
|
||||
changeField(this);
|
||||
}).on('click', '#change_all', function () {
|
||||
var tag_fields = $('.field-tag');
|
||||
var $this = $(this);
|
||||
var active = '1';
|
||||
if ($this.attr('value') == '0'){
|
||||
active = '0';
|
||||
$this.attr('value', '1').addClass('label-primary')
|
||||
} else {
|
||||
active = '1';
|
||||
$this.attr('value', '0').removeClass('label-primary')
|
||||
}
|
||||
$.each(tag_fields, function (k, v) {
|
||||
changeField(v, active)
|
||||
})
|
||||
});
|
||||
|
||||
function changeField(obj, active) {
|
||||
var $this = $(obj);
|
||||
var field_id = $this.data('id');
|
||||
if (!active) {
|
||||
active = $this.attr('value');
|
||||
}
|
||||
if (active == '0') {
|
||||
$this.attr('value', '1').addClass('label-primary');
|
||||
var form_groups = $('#add_form .form-group:not(.abc)');
|
||||
form_groups.filter(':has(#' + field_id + ')').show().find('select,input').prop('disabled', false)
|
||||
} else {
|
||||
$this.attr('value', '0').removeClass('label-primary');
|
||||
var form_groups = $('#add_form .form-group:not(.abc)');
|
||||
form_groups.filter(':has(#' + field_id + ')').hide().find('select,input').prop('disabled', true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
90
apps/assets/templates/assets/asset_create.html
Normal file
90
apps/assets/templates/assets/asset_create.html
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load asset_tags %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block form %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||
{% bootstrap_field form.ip layout="horizontal" %}
|
||||
{% bootstrap_field form.port layout="horizontal" %}
|
||||
{% bootstrap_field form.platform layout="horizontal" %}
|
||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
{% bootstrap_field form.admin_user layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Node' %}</h3>
|
||||
{% bootstrap_field form.nodes layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Labels' %}</h3>
|
||||
<div class="form-group {% if form.errors.labels %} has-error {% endif %}">
|
||||
<label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label>
|
||||
<div class="col-md-9">
|
||||
<select name="labels" class="select2 labels" data-placeholder="{% trans 'Label' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}">
|
||||
{% for name, labels in form.labels.field.queryset|group_labels %}
|
||||
<optgroup label="{{ name }}">
|
||||
{% for label in labels %}
|
||||
{% if label in form.labels.initial %}
|
||||
<option value="{{ label.id }}" selected>{{ label.value }}</option>
|
||||
{% else %}
|
||||
<option value="{{ label.id }}">{{ label.value }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if form.errors.labels %}
|
||||
{% for e in form.errors.labels %}
|
||||
<div class="help-block">{{ e }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Other' %}</h3>
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
{% bootstrap_field form.is_active layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function format(item) {
|
||||
var group = item.element.parentElement.label;
|
||||
return group + ':' + item.text;
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2({
|
||||
allowClear: true
|
||||
});
|
||||
$(".labels").select2({
|
||||
allowClear: true,
|
||||
templateSelection: format
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
364
apps/assets/templates/assets/asset_detail.html
Normal file
364
apps/assets/templates/assets/asset_detail.html
Normal file
@@ -0,0 +1,364 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
|
||||
<link href='{% static "css/plugins/sweetalert/sweetalert.css" %}' rel="stylesheet">
|
||||
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
|
||||
<script src='{% static "js/plugins/sweetalert/sweetalert.min.js" %}'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
|
||||
</li>
|
||||
{% if user.is_superuser %}
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-asset">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-7" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ asset.hostname }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="20%">{% trans 'Hostname' %}:</td>
|
||||
<td><b>{{ asset.hostname }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'IP' %}:</td>
|
||||
<td><b>{{ asset.ip }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Public IP' %}:</td>
|
||||
<td><b>{{ asset.public_ip|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Port' %}:</td>
|
||||
<td><b>{{ asset.port }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Admin user' %}:</td>
|
||||
<td><b>{{ asset.admin_user }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Vendor' %}:</td>
|
||||
<td><b>{{ asset.vendor|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Model' %}:</td>
|
||||
<td><b>{{ asset.model|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'CPU' %}:</td>
|
||||
<td><b>{{ asset.cpu_model|default:"" }} {{ asset.cpu_count|default:"" }}*{{ asset.cpu_cores|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Memory' %}:</td>
|
||||
<td><b>{{ asset.memory|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Disk' %}:</td>
|
||||
<td><b>{{ asset.disk_total|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Platform' %}:</td>
|
||||
<td><b>{{ asset.platform|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'OS' %}:</td>
|
||||
<td><b>{{ asset.os|default:"" }} {{ asset.os_version|default:"" }} {{ asset.os_arch|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Is active' %}:</td>
|
||||
<td><b>{{ asset.is_active|yesno:"Yes,No" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Serial number' %}:</td>
|
||||
<td><b>{{ asset.sn|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Asset number' %}:</td>
|
||||
<td><b>{{ asset.number|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ asset.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date joined' %}:</td>
|
||||
<td><b>{{ asset.date_joined|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ asset.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_superuser %}
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Active' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" {% if asset.is_active %} checked {% endif %} class="onoffswitch-checkbox" id="is_active">
|
||||
<label class="onoffswitch-label" for="is_active">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% if asset.is_unixlike %}
|
||||
<tr>
|
||||
<td>{% trans 'Refresh hardware' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_refresh_asset" style="width: 54px">{% trans 'Refresh' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-test-is-alive" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table group_edit" id="add-asset2group">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Nodes' %}" id="groups_selected" class="select2 groups" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes_remain %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-sm" id="btn-update-nodes">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for node in asset.nodes.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.name }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn-leave-node" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Labels' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ul class="tag-list" style="padding: 0">
|
||||
{% for label in asset.labels.all %}
|
||||
<li ><a href=""><i class="fa fa-tag"></i> {{ label.name }}:{{ label.value }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
jumpserver.nodes_selected = {};
|
||||
function updateAssetNodes(nodes) {
|
||||
var the_url = "{% url 'api-assets:asset-detail' pk=asset.id %}";
|
||||
var body = {
|
||||
nodes: Object.assign([], nodes)
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#groups_selected').val('');
|
||||
$.map(jumpserver.nodes_selected, function(group_name, index) {
|
||||
$('#opt_' + index).remove();
|
||||
// change tr html of user groups.
|
||||
$('#add-asset2group tbody').append(
|
||||
'<tr>' +
|
||||
'<td><b class="bdg_node" data-gid="' + index + '">' + group_name + '</b></td>' +
|
||||
'<td><button class="btn btn-danger btn-xs pull-right btn-leave-node" type="button"><i class="fa fa-minus"></i></button></td>' +
|
||||
'</tr>'
|
||||
)
|
||||
});
|
||||
// clear jumpserver.groups_selected
|
||||
jumpserver.nodes_selected = {};
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAssetHardware() {
|
||||
var the_url = "{% url 'api-assets:asset-refresh' pk=asset.id %}";
|
||||
var success = function(data) {
|
||||
console.log(data);
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
success: success,
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2.groups').select2().on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.nodes_selected[data.id] = data.text;
|
||||
}).on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id]
|
||||
});
|
||||
}).on('click', '#is_active', function () {
|
||||
var the_url = '{% url "api-assets:asset-detail" pk=asset.id %}';
|
||||
var checked = $(this).prop('checked');
|
||||
var body = {
|
||||
'is_active': checked
|
||||
};
|
||||
var success = '{% trans "Update successfully!" %}';
|
||||
var status = $(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").text();
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success_message: success
|
||||
});
|
||||
if (status === "False") {
|
||||
$(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('True');
|
||||
}else{
|
||||
$(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('False');
|
||||
}
|
||||
}).on('click', '#btn-update-nodes', function () {
|
||||
if (Object.keys(jumpserver.nodes_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var nodes = $('.bdg_node').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
nodes.push(index);
|
||||
$('#opt_' + index).remove();
|
||||
});
|
||||
updateAssetNodes(nodes)
|
||||
}).on('click', '.btn-leave-node', function() {
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $badge = $tr.find('.bdg_node');
|
||||
var gid = $badge.data('gid');
|
||||
var group_name = $badge.html() || $badge.text();
|
||||
$('#groups_selected').append(
|
||||
'<option value="' + gid + '" id="opt_' + gid + '">' + group_name + '</option>'
|
||||
);
|
||||
$tr.remove();
|
||||
var groups = $('.bdg_node').map(function () {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
updateAssetNodes(groups)
|
||||
}).on('click', '.btn-delete-asset', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ asset.hostname }}";
|
||||
var uid = "{{ asset.id }}";
|
||||
var the_url = '{% url "api-assets:asset-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'assets:asset-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
}).on('click', '#btn_refresh_asset', function () {
|
||||
refreshAssetHardware()
|
||||
}).on('click', '#btn-test-is-alive', function () {
|
||||
var the_url = "{% url 'api-assets:asset-alive-test' pk=asset.id %}";
|
||||
|
||||
var success = function(data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
};
|
||||
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success
|
||||
});
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
714
apps/assets/templates/assets/asset_list.html
Normal file
714
apps/assets/templates/assets/asset_list.html
Normal file
@@ -0,0 +1,714 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<style type="text/css">
|
||||
div#rMenu {
|
||||
position:absolute;
|
||||
visibility:hidden;
|
||||
text-align: left;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
float: left;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
div#rMenu li{
|
||||
margin: 1px 0;
|
||||
cursor: pointer;
|
||||
{#list-style: none outside none;#}
|
||||
}
|
||||
.dropdown a:hover {
|
||||
background-color: #f1f1f1
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9 animated fadeInRight" id="split-right">
|
||||
<div class="tree-toggle">
|
||||
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
|
||||
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-box-header">
|
||||
<div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div>
|
||||
<div class="html5buttons">
|
||||
<div class="dt-buttons btn-group">
|
||||
<a class="btn btn-default btn_import" data-toggle="modal" data-target="#asset_import_modal" tabindex="0">
|
||||
<span>{% trans "Import" %}</span>
|
||||
</a>
|
||||
<a class="btn btn-default btn_export" tabindex="0">
|
||||
<span>{% trans "Export" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" style="float: right">
|
||||
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu labels">
|
||||
{% for label in labels %}
|
||||
<li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="asset_list_table" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Hardware' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="actions" class="hide">
|
||||
<div class="input-group">
|
||||
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
|
||||
<option value="delete">{% trans 'Delete selected' %}</option>
|
||||
<option value="update">{% trans 'Update selected' %}</option>
|
||||
<option value="remove">{% trans 'Remove from this node' %}</option>
|
||||
<option value="deactive">{% trans 'Deactive selected' %}</option>
|
||||
<option value="active">{% trans 'Active selected' %}</option>
|
||||
</select>
|
||||
<div class="input-group-btn pull-left" style="padding-left: 5px;">
|
||||
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
|
||||
{% trans 'Submit' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rMenu">
|
||||
<ul class="dropdown-menu">
|
||||
<li class="divider"></li>
|
||||
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
|
||||
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
|
||||
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li id="menu_asset_add" class="btn-add-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-copy"></i> {% trans 'Add assets to node' %}</a></li>
|
||||
<li id="menu_asset_move" class="btn-move-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-cut"></i> {% trans 'Move assets to node' %}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
|
||||
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% include 'assets/_asset_import_modal.html' %}
|
||||
{% include 'assets/_asset_list_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var zTree, rMenu, asset_table, show = 0;
|
||||
var update_node_action = "";
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
|
||||
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
$(td).html(rowData.hardware_info)
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
if (cellData === 'Unknown'){
|
||||
$(td).html('<i class="fa fa-circle text-warning"></i>')
|
||||
} else if (!cellData) {
|
||||
$(td).html('<i class="fa fa-circle text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-circle text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:asset-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "cpu_cores"}, {data: "is_active", orderable: false },
|
||||
{data: "is_connective", orderable: false}, {data: "id", orderable: false }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
asset_table = jumpserver.initServerSideDataTable(options);
|
||||
return asset_table
|
||||
}
|
||||
|
||||
function addTreeNode() {
|
||||
hideRMenu();
|
||||
var parentNode = zTree.getSelectedNodes()[0];
|
||||
if (!parentNode){
|
||||
return
|
||||
}
|
||||
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.id );
|
||||
$.post(url, {}, function (data, status){
|
||||
if (status === "success") {
|
||||
var newNode = {
|
||||
name: data["value"],
|
||||
id: data["id"],
|
||||
pId: parentNode.id
|
||||
};
|
||||
newNode.checked = zTree.getSelectedNodes()[0].checked;
|
||||
zTree.addNodes(parentNode, 0, newNode);
|
||||
} else {
|
||||
alert("{% trans 'Create node failed' %}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTreeNode() {
|
||||
hideRMenu();
|
||||
var current_node = zTree.getSelectedNodes()[0];
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
if (current_node.children && current_node.children.length > 0) {
|
||||
toastr.error("{% trans 'Have child node, cancel' %}");
|
||||
} else if (current_node.assets_amount !== 0) {
|
||||
toastr.error("{% trans 'Have assets, cancel' %}");
|
||||
} else {
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.id );
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "DELETE",
|
||||
success: function () {
|
||||
zTree.removeNode(current_node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editTreeNode() {
|
||||
hideRMenu();
|
||||
var current_node = zTree.getSelectedNodes()[0];
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
if (current_node.value) {
|
||||
current_node.name = current_node.value;
|
||||
}
|
||||
zTree.editName(current_node);
|
||||
}
|
||||
|
||||
function OnRightClick(event, treeId, treeNode) {
|
||||
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
|
||||
zTree.cancelSelectedNode();
|
||||
showRMenu("root", event.clientX, event.clientY);
|
||||
} else if (treeNode && !treeNode.noR) {
|
||||
zTree.selectNode(treeNode);
|
||||
showRMenu("node", event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function showRMenu(type, x, y) {
|
||||
$("#rMenu ul").show();
|
||||
x -= 220;
|
||||
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
|
||||
|
||||
$("body").bind("mousedown", onBodyMouseDown);
|
||||
}
|
||||
|
||||
function beforeClick(treeId, treeNode, clickFlag) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideRMenu() {
|
||||
if (rMenu) rMenu.css({"visibility": "hidden"});
|
||||
$("body").unbind("mousedown", onBodyMouseDown);
|
||||
}
|
||||
|
||||
function onBodyMouseDown(event){
|
||||
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRename(event, treeId, treeNode, isCancel){
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.id);
|
||||
var data = {"value": treeNode.name};
|
||||
if (isCancel){
|
||||
return
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(data),
|
||||
method: "PATCH"
|
||||
})
|
||||
}
|
||||
|
||||
function onSelected(event, treeNode) {
|
||||
var url = asset_table.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
asset_table.ajax.url(url);
|
||||
asset_table.ajax.reload();
|
||||
}
|
||||
|
||||
function selectQueryNode() {
|
||||
var query_node_id = $.getUrlParam("node");
|
||||
var cookie_node_id = getCookie('node_selected');
|
||||
var node;
|
||||
var node_id;
|
||||
|
||||
if (query_node_id !== null) {
|
||||
node_id = query_node_id
|
||||
} else if (cookie_node_id !== null) {
|
||||
node_id = cookie_node_id;
|
||||
}
|
||||
|
||||
node = zTree.getNodesByParam("id", node_id, null);
|
||||
if (node){
|
||||
zTree.selectNode(node[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function beforeDrag() {
|
||||
return true
|
||||
}
|
||||
|
||||
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesNames = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesNames.push(value.value);
|
||||
});
|
||||
|
||||
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
|
||||
if (confirm(msg)){
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function onDrag(event, treeId, treeNodes) {
|
||||
}
|
||||
|
||||
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesIds = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesIds.push(value.id);
|
||||
});
|
||||
|
||||
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.id);
|
||||
var body = {nodes: treeNodesIds};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
}
|
||||
|
||||
function initTree() {
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isCopy: true,
|
||||
isMove: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onRightClick: OnRightClick,
|
||||
beforeClick: beforeClick,
|
||||
onRename: onRename,
|
||||
onSelected: onSelected,
|
||||
beforeDrag: beforeDrag,
|
||||
onDrag: onDrag,
|
||||
beforeDrop: beforeDrop,
|
||||
onDrop: onDrop
|
||||
}
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-assets:node-list' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["pId"] = value["parent"];
|
||||
{#if (value["key"] === "0") {#}
|
||||
value["open"] = true;
|
||||
{# }#}
|
||||
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
|
||||
value['value'] = value['value'];
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
rMenu = $("#rMenu");
|
||||
selectQueryNode();
|
||||
});
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (show === 0) {
|
||||
$("#split-left").hide(500, function () {
|
||||
$("#split-right").attr("class", "col-lg-12");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
|
||||
show = 1;
|
||||
});
|
||||
} else {
|
||||
$("#split-right").attr("class", "col-lg-9");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
|
||||
$("#split-left").show(500);
|
||||
show = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
initTree();
|
||||
})
|
||||
.on('click', '.labels li', function () {
|
||||
var val = $(this).text();
|
||||
$("#asset_list_table_filter input").val(val);
|
||||
asset_table.search(val).draw();
|
||||
})
|
||||
.on('click', '.btn_export', function () {
|
||||
var $data_table = $('#asset_list_table').DataTable();
|
||||
var rows = $data_table.rows('.selected').data();
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0];
|
||||
}
|
||||
var assets = [];
|
||||
$.each(rows, function (index, obj) {
|
||||
assets.push(obj.id)
|
||||
});
|
||||
$.ajax({
|
||||
url: "{% url "assets:asset-export" %}",
|
||||
method: 'POST',
|
||||
data: JSON.stringify({assets_id: assets, node_id: current_node.id}),
|
||||
dataType: "json",
|
||||
success: function (data, textStatus) {
|
||||
window.open(data.redirect)
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Export failed');
|
||||
}
|
||||
})
|
||||
})
|
||||
.on('click', '#btn_asset_import', function () {
|
||||
var $form = $('#fm_asset_import');
|
||||
var action = $form.attr("action");
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
action = setUrlParam(action, 'node_id', current_node.id);
|
||||
{#action += "?node_id=" + current_node.id;#}
|
||||
$form.attr("action", action)
|
||||
}
|
||||
$form.find('.help-block').remove();
|
||||
function success (data) {
|
||||
if (data.valid === false) {
|
||||
$('<span />', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_assets'));
|
||||
} else {
|
||||
$('#id_created').html(data.created_info);
|
||||
$('#id_created_detail').html(data.created.join(', '));
|
||||
$('#id_updated').html(data.updated_info);
|
||||
$('#id_updated_detail').html(data.updated.join(', '));
|
||||
$('#id_failed').html(data.failed_info);
|
||||
$('#id_failed_detail').html(data.failed.join(', '));
|
||||
var $data_table = $('#asset_list_table').DataTable();
|
||||
$data_table.ajax.reload();
|
||||
}
|
||||
}
|
||||
$form.ajaxSubmit({success: success});
|
||||
})
|
||||
.on('click', '.btn-create-asset', function () {
|
||||
var url = "{% url 'assets:asset-create' %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
url += "?node_id=" + current_node.id;
|
||||
}
|
||||
window.open(url, '_self');
|
||||
})
|
||||
.on('click', '.btn-refresh-hardware', function () {
|
||||
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "GET",
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "GET",
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn_asset_delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $("#asset_list_table").DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:asset-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
})
|
||||
.on('click', '#btn_bulk_update', function () {
|
||||
var action = $('#slct_bulk_update').val();
|
||||
var $data_table = $('#asset_list_table').DataTable();
|
||||
var id_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
id_list.push(this.data().id);
|
||||
});
|
||||
if (id_list.length === 0) {
|
||||
return false;
|
||||
}
|
||||
var the_url = "{% url 'api-assets:asset-list' %}";
|
||||
|
||||
function doDeactive() {
|
||||
var data = [];
|
||||
$.each(id_list, function(index, object_id) {
|
||||
var obj = {"pk": object_id, "is_active": false};
|
||||
data.push(obj);
|
||||
});
|
||||
function success() {
|
||||
asset_table.ajax.reload()
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
function doActive() {
|
||||
var data = [];
|
||||
$.each(id_list, function(index, object_id) {
|
||||
var obj = {"pk": object_id, "is_active": true};
|
||||
data.push(obj);
|
||||
});
|
||||
function success() {
|
||||
asset_table.ajax.reload()
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
function doDelete() {
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected assets !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
var success = function() {
|
||||
var msg = "{% trans 'Asset Deleted.' %}";
|
||||
swal("{% trans 'Asset Delete' %}", msg, "success");
|
||||
$('#asset_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'Asset Deleting failed.' %}";
|
||||
swal("{% trans 'Asset Delete' %}", msg, "error");
|
||||
};
|
||||
var url_delete = the_url + '?id__in=' + JSON.stringify(id_list);
|
||||
APIUpdateAttr({
|
||||
url: url_delete,
|
||||
method: 'DELETE',
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
$data_table.ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
});
|
||||
}
|
||||
function doUpdate() {
|
||||
var id_list_string = id_list.join(',');
|
||||
var url = "{% url 'assets:asset-bulk-update' %}?assets_id=" + id_list_string;
|
||||
location.href = url
|
||||
}
|
||||
|
||||
function doRemove() {
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
var data = {
|
||||
'assets': id_list
|
||||
};
|
||||
|
||||
var success = function () {
|
||||
asset_table.ajax.reload()
|
||||
};
|
||||
|
||||
APIUpdateAttr({
|
||||
'url': '/api/assets/v1/nodes/' + current_node.id + '/assets/remove/',
|
||||
'method': 'PUT',
|
||||
'body': JSON.stringify(data),
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
switch(action) {
|
||||
case 'deactive':
|
||||
doDeactive();
|
||||
break;
|
||||
case 'delete':
|
||||
doDelete();
|
||||
break;
|
||||
case 'update':
|
||||
doUpdate();
|
||||
break;
|
||||
case 'active':
|
||||
doActive();
|
||||
break;
|
||||
case 'remove':
|
||||
doRemove();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
$(".ipt_check_all").prop("checked", false)
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets_selected = asset_table2.selected;
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
var data = {
|
||||
'assets': assets_selected
|
||||
};
|
||||
var success = function () {
|
||||
asset_table2.selected = [];
|
||||
asset_table2.ajax.reload()
|
||||
};
|
||||
|
||||
var url = '';
|
||||
if (update_node_action === "move") {
|
||||
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.id);
|
||||
} else {
|
||||
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.id);
|
||||
}
|
||||
|
||||
APIUpdateAttr({
|
||||
'url': url,
|
||||
'method': 'PUT',
|
||||
'body': JSON.stringify(data),
|
||||
'success': success
|
||||
})
|
||||
}).on('hidden.bs.modal', '#asset_list_modal', function () {
|
||||
window.location.reload();
|
||||
}).on('click', '#menu_asset_add', function () {
|
||||
update_node_action = "add"
|
||||
}).on('click', '#menu_asset_move', function () {
|
||||
update_node_action = "move"
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
94
apps/assets/templates/assets/asset_update.html
Normal file
94
apps/assets/templates/assets/asset_update.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load asset_tags %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block custom_head_css_js_create %}
|
||||
<link href="{% static "css/plugins/inputTags.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/inputTags.jquery.min.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% if form.no_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.hostname layout="horizontal" %}
|
||||
{% bootstrap_field form.ip layout="horizontal" %}
|
||||
{% bootstrap_field form.port layout="horizontal" %}
|
||||
{% bootstrap_field form.platform layout="horizontal" %}
|
||||
{% bootstrap_field form.public_ip layout="horizontal" %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
{% bootstrap_field form.admin_user layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Node' %}</h3>
|
||||
{% bootstrap_field form.nodes layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Labels' %}</h3>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label>
|
||||
<div class="col-md-9">
|
||||
<select name="labels" class="select2 labels" data-placeholder="{% trans 'Label' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}">
|
||||
{% for name, labels in form.labels.field.queryset|group_labels %}
|
||||
<optgroup label="{{ name }}">
|
||||
{% for label in labels %}
|
||||
{% if label in form.labels.initial %}
|
||||
<option value="{{ label.id }}" selected>{{ label.value }}</option>
|
||||
{% else %}
|
||||
<option value="{{ label.id }}">{{ label.value }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Configuration' %}</h3>
|
||||
{% bootstrap_field form.number layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Other' %}</h3>
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
{% bootstrap_field form.is_active layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function format(item) {
|
||||
var group = item.element.parentElement.label;
|
||||
return group + ':' + item.text;
|
||||
}
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2({
|
||||
allowClear: true
|
||||
});
|
||||
$(".labels").select2({
|
||||
allowClear: true,
|
||||
templateSelection: format
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
apps/assets/templates/assets/delete_confirm.html
Normal file
15
apps/assets/templates/assets/delete_confirm.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans 'Confirm delete' %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>{% trans 'Are you sure delete' %} <b>{{ object.name }} </b> ?</p>
|
||||
<input type="submit" value="Confirm" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
42
apps/assets/templates/assets/domain_create_update.html
Normal file
42
apps/assets/templates/assets/domain_create_update.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<form id="groupForm" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.assets layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include 'assets/_asset_list_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
console.log($.fn.select2.defaults);
|
||||
$('.select2').select2().off("select2:open");
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets = asset_table2.selected;
|
||||
$.each(assets, function (id, data) {
|
||||
$('.select2').val(assets).trigger('change');
|
||||
});
|
||||
$("#asset_list_modal").modal('hide');
|
||||
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
132
apps/assets/templates/assets/domain_detail.html
Normal file
132
apps/assets/templates/assets/domain_detail.html
Normal file
@@ -0,0 +1,132 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
|
||||
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:domain-gateway-list' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:domain-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-del">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-9" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ object.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ object.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Asset' %}:</td>
|
||||
<td><b>{{ object.assets.count }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Gateway' %}:</td>
|
||||
<td><b>{{ object.gateway_set.count }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ object.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ object.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ object.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#domain_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:domain-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:domain-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "name" }, {data: "asset_count" },
|
||||
{data: "gateway_count" }, {data: "comment" }, {data: "id"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#domain_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:domain-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
126
apps/assets/templates/assets/domain_gateway_list.html
Normal file
126
apps/assets/templates/assets/domain_gateway_list.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
|
||||
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-12" style="padding-left: 0;">
|
||||
<div class="" id="content_start">
|
||||
</div>
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left"><b>{% trans 'Gateway list' %}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}" class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="domain_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Port' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#domain_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:domain-gateway-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + test_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:gateway-list" %}?domain={{ object.id }}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "name" }, {data: 'ip'}, {data: 'port'},
|
||||
{data: "protocol"}, {data: "username" }, {data: "comment" }, {data: "id"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#domain_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:gateway-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
}).on('click', '.btn-test', function () {
|
||||
var $this = $(this);
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "GET",
|
||||
success_message: "可连接",
|
||||
fail_message: "连接失败"
|
||||
})
|
||||
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
74
apps/assets/templates/assets/domain_list.html
Normal file
74
apps/assets/templates/assets/domain_list.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="domain_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Gateway' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#domain_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var gateway_list_btn = '<a href="{% url "assets:domain-gateway-list" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
gateway_list_btn = gateway_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
|
||||
$(td).html(gateway_list_btn);
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:domain-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:domain-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "name" }, {data: "asset_count" },
|
||||
{data: "gateway_count" }, {data: "comment" }, {data: "id"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#domain_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:domain-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
68
apps/assets/templates/assets/gateway_create_update.html
Normal file
68
apps/assets/templates/assets/gateway_create_update.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.ip layout="horizontal" %}
|
||||
{% bootstrap_field form.port layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
<div class="auth-fields">
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<h3>{% trans 'Other' %}</h3>
|
||||
{% bootstrap_field form.is_active layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
47
apps/assets/templates/assets/label_create_update.html
Normal file
47
apps/assets/templates/assets/label_create_update.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
|
||||
{% block form %}
|
||||
<form id="groupForm" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.value layout="horizontal" %}
|
||||
{% bootstrap_field form.assets layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include 'assets/_asset_list_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2({
|
||||
closeOnSelect: false
|
||||
})
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets = asset_table2.selected;
|
||||
$('.select2 option:selected').each(function (i, data) {
|
||||
assets.push($(data).attr('value'))
|
||||
});
|
||||
$.each(assets, function (id, data) {
|
||||
$('.select2').val(assets).trigger('change');
|
||||
});
|
||||
$("#asset_list_modal").modal('hide');
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
70
apps/assets/templates/assets/label_list.html
Normal file
70
apps/assets/templates/assets/label_list.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:label-create' %}" class="btn btn-sm btn-primary"> {% trans "Create label" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="label_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Value' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#label_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{# var detail_btn = '<a href="{% url "assets:label-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';#}
|
||||
var detail_btn = '<a>' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:label-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:label-list" %}?sort=name',
|
||||
columns: [
|
||||
{data: "id"}, {data: "name" }, {data: "value" },
|
||||
{data: "asset_count" }, {data: "id"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#label_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:label-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
166
apps/assets/templates/assets/system_user_asset.html
Normal file
166
apps/assets/templates/assets/system_user_asset.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
|
||||
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Assets of ' %} <b>{{ system_user.name }} </b><span class="badge">{{ paginator.count }}</span></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table table-hover" id="system_user_list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Reachable' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Push system user now' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Test assets connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetsTable() {
|
||||
var unreachable = {{ system_user.unreachable_assets|safe}};
|
||||
var options = {
|
||||
ele: $('#system_user_list'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 0, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (unreachable.indexOf(cellData) >= 0) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}',
|
||||
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on("select2:select", function (evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.assets_selected[data.id] = data.text;
|
||||
jumpserver.asset_groups_selected[data.id] = data.text;
|
||||
})
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.assets_selected[data.id];
|
||||
delete jumpserver.asset_groups_selected[data.id];
|
||||
});
|
||||
initAssetsTable();
|
||||
})
|
||||
.on('click', '.btn-push', function () {
|
||||
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
|
||||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
error: error,
|
||||
method: 'GET',
|
||||
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
|
||||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
error: error,
|
||||
method: 'GET',
|
||||
success_message: "{% trans "Task has been send, seen left assets status" %}"
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
7
apps/assets/templates/assets/system_user_create.html
Normal file
7
apps/assets/templates/assets/system_user_create.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'assets/_system_user.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block auth %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
323
apps/assets/templates/assets/system_user_detail.html
Normal file
323
apps/assets/templates/assets/system_user_detail.html
Normal file
@@ -0,0 +1,323 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
|
||||
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
{# <li>#}
|
||||
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#}
|
||||
{# <i class="fa fa-bar-chart-o"></i> {% trans 'Attached assets' %}#}
|
||||
{# </a>#}
|
||||
{# </li>#}
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-del">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ system_user.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ system_user.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Username' %}:</td>
|
||||
<td><b>{{ system_user.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Protocol' %}:</td>
|
||||
<td><b>{{ system_user.protocol }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Sudo' %}:</td>
|
||||
<td><b>{{ system_user.sudo }}</b></td>
|
||||
</tr>
|
||||
{% if system_user.shell %}
|
||||
<tr>
|
||||
<td>{% trans 'Shell' %}:</td>
|
||||
<td><b>{{ system_user.shell }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if system_user.home %}
|
||||
<tr>
|
||||
<td>{% trans 'Home' %}:</td>
|
||||
<td><b>{{ system_user.home }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if system_user.uid %}
|
||||
<tr>
|
||||
<td>{% trans 'Uid' %}:</td>
|
||||
<td><b>{{ system_user.uid }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ system_user.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ asset_group.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ system_user.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Auto push' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" {% if system_user.auto_push %} checked {% endif %} class="onoffswitch-checkbox" id="btn-auto-push">
|
||||
<label class="onoffswitch-label" for="btn-auto-push">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
{% if system_user.auto_push %}
|
||||
<td width="50%">{% trans 'Push system user now' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% endif %}
|
||||
<td width="50%">{% trans 'Test assets connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
||||
{# <td>#}
|
||||
{# <span style="float: right">#}
|
||||
{# <button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>#}
|
||||
{# </span>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table node_edit" id="add-asset2group">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes_remain %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for node in system_user.nodes.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function updateSystemUserNode(nodes) {
|
||||
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
|
||||
var body = {
|
||||
nodes: Object.assign([], nodes)
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#node_selected').val('');
|
||||
$.map(jumpserver.nodes_selected, function(node_name, index) {
|
||||
$('#opt_' + index).remove();
|
||||
// change tr html of user groups.
|
||||
$('.node_edit tbody').append(
|
||||
'<tr>' +
|
||||
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
|
||||
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
|
||||
'</tr>'
|
||||
)
|
||||
});
|
||||
// clear jumpserver.groups_selected
|
||||
jumpserver.nodes_selected = {};
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
jumpserver.nodes_selected = {};
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.nodes_selected[data.id] = data.text;
|
||||
})
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id];
|
||||
});
|
||||
})
|
||||
.on('click', '#btn-auto-push', function () {
|
||||
var checked = $(this).prop('checked');
|
||||
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
|
||||
var body = {
|
||||
'auto_push': checked
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
})
|
||||
.on('click', '#btn-add-to-node', function() {
|
||||
if (Object.keys(jumpserver.nodes_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var nodes = $('.bdg_node').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
nodes.push(index);
|
||||
});
|
||||
updateSystemUserNode(nodes);
|
||||
})
|
||||
.on('click', '.btn-remove-from-node', function() {
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $badge = $tr.find('.bdg_node');
|
||||
var gid = $badge.data('gid');
|
||||
var node_name = $badge.html() || $badge.text();
|
||||
$('#groups_selected').append(
|
||||
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
|
||||
);
|
||||
$tr.remove();
|
||||
var nodes = $('.bdg_node').map(function () {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
updateSystemUserNode(nodes);
|
||||
}).on('click', '.btn-del', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ system_user.name}}";
|
||||
var uid = "{{ system_user.id }}";
|
||||
var the_url = '{% url "api-assets:system-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'assets:system-user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
.on('click', '.btn-push', function () {
|
||||
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
174
apps/assets/templates/assets/system_user_list.html
Normal file
174
apps/assets/templates/assets/system_user_list.html
Normal file
@@ -0,0 +1,174 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);
|
||||
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。
|
||||
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
|
||||
目前还不支持Windows的自动推送
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_search %}
|
||||
{% endblock %}
|
||||
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:system-user-create' %}" class="btn btn-sm btn-primary "> {% trans "Create system user" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="system_user_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Unreachable' %}</th>
|
||||
<th class="text-center">{% trans 'Ratio' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#system_user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
|
||||
} else {
|
||||
innerHtml = "<span>" + cellData + "</span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData !== 0) {
|
||||
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
|
||||
} else {
|
||||
innerHtml = "<span>" + cellData + "</span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var val = 0;
|
||||
var innerHtml = "";
|
||||
var total = rowData.assets_amount;
|
||||
var reachable = rowData.reachable_amount;
|
||||
if (total !== 0) {
|
||||
val = reachable/total * 100;
|
||||
}
|
||||
|
||||
if (val === 100) {
|
||||
innerHtml = "<span class='text-navy'>" + val + "% </span>";
|
||||
} else {
|
||||
var num = new Number(val);
|
||||
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
|
||||
}},
|
||||
{targets: 9, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||
columns: [
|
||||
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
|
||||
.on('click', '.btn_admin_user_delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#cluster_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:system-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
})
|
||||
|
||||
.on('click', '#btn_bulk_update', function () {
|
||||
var action = $('#slct_bulk_update').val();
|
||||
var $data_table = $('#system_user_list_table').DataTable();
|
||||
var id_list = [];
|
||||
var plain_id_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
id_list.push({id: this.data().id});
|
||||
plain_id_list.push(this.data().id);
|
||||
});
|
||||
if (id_list === []) {
|
||||
return false;
|
||||
}
|
||||
var the_url = "{% url 'api-assets:system-user-list' %}";
|
||||
function doDelete() {
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected System Users !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
var success = function() {
|
||||
var msg = "{% trans 'System Users Deleted.' %}";
|
||||
swal("{% trans 'System Users Delete' %}", msg, "success");
|
||||
$('#system_user_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'System Users Deleting failed.' %}";
|
||||
swal("{% trans 'System Users Delete' %}", msg, "error");
|
||||
};
|
||||
var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list);
|
||||
APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail});
|
||||
$data_table.ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
});
|
||||
}
|
||||
function doUpdate() {
|
||||
{# TODO: bulk update the System Users #}
|
||||
}
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
doDelete();
|
||||
break;
|
||||
case 'update':
|
||||
doUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
24
apps/assets/templates/assets/system_user_update.html
Normal file
24
apps/assets/templates/assets/system_user_update.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'assets/_system_user.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block auth %}
|
||||
<h3>{% trans 'Auth' %}</h3>
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key_file layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_push}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
171
apps/assets/templates/assets/user_asset_list.html
Normal file
171
apps/assets/templates/assets/user_asset_list.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9 animated fadeInRight" id="split-right">
|
||||
<div class="tree-toggle">
|
||||
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
|
||||
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-box-header">
|
||||
<div class="btn-group" style="float: right">
|
||||
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu labels">
|
||||
{% for label in labels %}
|
||||
<li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="user_assets_table" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'System users' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var zTree, rMenu, asset_table;
|
||||
var inited = false;
|
||||
var url;
|
||||
function initTable() {
|
||||
if (inited){
|
||||
return
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
var options = {
|
||||
ele: $('#user_assets_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
|
||||
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var users = [];
|
||||
$.each(cellData, function (id, data) {
|
||||
users.push(data.name);
|
||||
});
|
||||
$(td).html(users.join(', '))
|
||||
}}
|
||||
],
|
||||
ajax_url: url,
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "is_active", orderable: false },
|
||||
{data: "system_users_granted", orderable: false}
|
||||
]
|
||||
};
|
||||
asset_table = jumpserver.initDataTable(options);
|
||||
return asset_table
|
||||
}
|
||||
|
||||
function onSelected(event, treeNode) {
|
||||
console.log("select");
|
||||
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
|
||||
url = url.replace("{{ DEFAULT_PK }}", treeNode.id);
|
||||
initTable();
|
||||
setCookie('node_selected', treeNode.id);
|
||||
asset_table.ajax.url(url);
|
||||
asset_table.ajax.reload();
|
||||
}
|
||||
|
||||
function selectQueryNode() {
|
||||
var query_node_id = $.getUrlParam("node");
|
||||
var cookie_node_id = getCookie('node_selected');
|
||||
var node;
|
||||
var node_id;
|
||||
|
||||
if (query_node_id !== null) {
|
||||
node_id = query_node_id
|
||||
} else if (cookie_node_id !== null) {
|
||||
node_id = cookie_node_id;
|
||||
}
|
||||
|
||||
node = zTree.getNodesByParam("id", node_id, null);
|
||||
if (node){
|
||||
zTree.selectNode(node[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function initTree() {
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onSelected: onSelected
|
||||
}
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["pId"] = value["parent"];
|
||||
if (value["key"] === "0") {
|
||||
value["open"] = true;
|
||||
}
|
||||
value["name"] = value["value"]
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
rMenu = $("#rMenu");
|
||||
selectQueryNode();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
initTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
2
apps/assets/templatetags/__init__.py
Normal file
2
apps/assets/templatetags/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
12
apps/assets/templatetags/asset_tags.py
Normal file
12
apps/assets/templatetags/asset_tags.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from django import template
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def group_labels(queryset):
|
||||
grouped = defaultdict(list)
|
||||
for label in queryset:
|
||||
grouped[label.name].append(label)
|
||||
return [(name, labels) for name, labels in grouped.items()]
|
||||
1
apps/assets/urls/__init__.py
Normal file
1
apps/assets/urls/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
52
apps/assets/urls/api_urls.py
Normal file
52
apps/assets/urls/api_urls.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# coding:utf-8
|
||||
from django.conf.urls import url
|
||||
from .. import api
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
app_name = 'assets'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'v1/assets', api.AssetViewSet, 'asset')
|
||||
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
|
||||
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
|
||||
router.register(r'v1/labels', api.LabelViewSet, 'label')
|
||||
router.register(r'v1/nodes', api.NodeViewSet, 'node')
|
||||
router.register(r'v1/domain', api.DomainViewSet, 'domain')
|
||||
router.register(r'v1/gateway', api.GatewayViewSet, 'gateway')
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
|
||||
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth-info/', api.SystemUserAuthInfoApi.as_view(),
|
||||
name='system-user-auth-info'),
|
||||
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/refresh/$',
|
||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$',
|
||||
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
||||
url(r'^v1/assets/user-assets/$',
|
||||
api.UserAssetListView.as_view(), name='user-asset-list'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
|
||||
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
|
||||
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
|
||||
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
|
||||
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/push/$',
|
||||
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
||||
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
|
||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
url(r'^v1/nodes/children/$', api.NodeChildrenApi.as_view(), name='node-children-2'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', api.NodeAssetsApi.as_view(), name='node-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
|
||||
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
|
||||
|
||||
url(r'^v1/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
||||
53
apps/assets/urls/views_urls.py
Normal file
53
apps/assets/urls/views_urls.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# coding:utf-8
|
||||
from django.conf.urls import url
|
||||
from .. import views
|
||||
|
||||
app_name = 'assets'
|
||||
|
||||
urlpatterns = [
|
||||
# Resource asset url
|
||||
url(r'^$', views.AssetListView.as_view(), name='asset-index'),
|
||||
url(r'^asset/$', views.AssetListView.as_view(), name='asset-list'),
|
||||
url(r'^asset/create/$', views.AssetCreateView.as_view(), name='asset-create'),
|
||||
url(r'^asset/export/$', views.AssetExportView.as_view(), name='asset-export'),
|
||||
url(r'^asset/import/$', views.BulkImportAssetView.as_view(), name='asset-import'),
|
||||
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AssetDetailView.as_view(), name='asset-detail'),
|
||||
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AssetUpdateView.as_view(), name='asset-update'),
|
||||
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AssetDeleteView.as_view(), name='asset-delete'),
|
||||
url(r'^asset/update/$', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
|
||||
|
||||
# User asset view
|
||||
url(r'^user-asset/$', views.UserAssetListView.as_view(), name='user-asset-list'),
|
||||
|
||||
# Resource admin user url
|
||||
url(r'^admin-user/$', views.AdminUserListView.as_view(), name='admin-user-list'),
|
||||
url(r'^admin-user/create/$', views.AdminUserCreateView.as_view(), name='admin-user-create'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.AdminUserAssetsView.as_view(), name='admin-user-assets'),
|
||||
|
||||
# Resource system user url
|
||||
url(r'^system-user/$', views.SystemUserListView.as_view(), name='system-user-list'),
|
||||
url(r'^system-user/create/$', views.SystemUserCreateView.as_view(), name='system-user-create'),
|
||||
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.SystemUserDetailView.as_view(), name='system-user-detail'),
|
||||
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.SystemUserUpdateView.as_view(), name='system-user-update'),
|
||||
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
|
||||
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/asset/$', views.SystemUserAssetView.as_view(), name='system-user-asset'),
|
||||
|
||||
url(r'^label/$', views.LabelListView.as_view(), name='label-list'),
|
||||
url(r'^label/create/$', views.LabelCreateView.as_view(), name='label-create'),
|
||||
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.LabelUpdateView.as_view(), name='label-update'),
|
||||
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.LabelDeleteView.as_view(), name='label-delete'),
|
||||
|
||||
url(r'^domain/$', views.DomainListView.as_view(), name='domain-list'),
|
||||
url(r'^domain/create/$', views.DomainCreateView.as_view(), name='domain-create'),
|
||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.DomainDetailView.as_view(), name='domain-detail'),
|
||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainUpdateView.as_view(), name='domain-update'),
|
||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.DomainDeleteView.as_view(), name='domain-delete'),
|
||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/$', views.DomainGatewayListView.as_view(), name='domain-gateway-list'),
|
||||
|
||||
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
|
||||
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
|
||||
]
|
||||
|
||||
81
apps/assets/utils.py
Normal file
81
apps/assets/utils.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
|
||||
import paramiko
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from .models import Asset, SystemUser, Label
|
||||
|
||||
|
||||
def get_assets_by_id_list(id_list):
|
||||
return Asset.objects.filter(id__in=id_list)
|
||||
|
||||
|
||||
def get_assets_by_hostname_list(hostname_list):
|
||||
return Asset.objects.filter(hostname__in=hostname_list)
|
||||
|
||||
|
||||
def get_system_user_by_name(name):
|
||||
system_user = get_object_or_none(SystemUser, name=name)
|
||||
return system_user
|
||||
|
||||
|
||||
class LabelFilter:
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
query_keys = self.request.query_params.keys()
|
||||
all_label_keys = Label.objects.values_list('name', flat=True)
|
||||
valid_keys = set(all_label_keys) & set(query_keys)
|
||||
labels_query = {}
|
||||
for key in valid_keys:
|
||||
labels_query[key] = self.request.query_params.get(key)
|
||||
|
||||
conditions = []
|
||||
for k, v in labels_query.items():
|
||||
query = {'labels__name': k, 'labels__value': v}
|
||||
conditions.append(query)
|
||||
|
||||
if conditions:
|
||||
for kwargs in conditions:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
return queryset
|
||||
|
||||
|
||||
def test_gateway_connectability(gateway):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
:param gateway:
|
||||
:return:
|
||||
"""
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
proxy_command = [
|
||||
"ssh", "{}@{}".format(gateway.username, gateway.ip),
|
||||
"-p", str(gateway.port), "-W", "127.0.0.1:{}".format(gateway.port),
|
||||
]
|
||||
|
||||
if gateway.password:
|
||||
proxy_command.insert(0, "sshpass -p '{}'".format(gateway.password))
|
||||
if gateway.private_key:
|
||||
proxy_command.append("-i {}".format(gateway.private_key_file))
|
||||
|
||||
try:
|
||||
sock = paramiko.ProxyCommand(" ".join(proxy_command))
|
||||
except paramiko.ProxyCommandFailure as e:
|
||||
return False, str(e)
|
||||
|
||||
try:
|
||||
client.connect("127.0.0.1", port=gateway.port,
|
||||
username=gateway.username,
|
||||
password=gateway.password,
|
||||
key_filename=gateway.private_key_file,
|
||||
sock=sock,
|
||||
timeout=5
|
||||
)
|
||||
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
|
||||
paramiko.AuthenticationException, TimeoutError) as e:
|
||||
return False, str(e)
|
||||
finally:
|
||||
client.close()
|
||||
return True, None
|
||||
6
apps/assets/views/__init__.py
Normal file
6
apps/assets/views/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# coding:utf-8
|
||||
from .asset import *
|
||||
from .system_user import *
|
||||
from .admin_user import *
|
||||
from .label import *
|
||||
from .domain import *
|
||||
116
apps/assets/views/admin_user.py
Normal file
116
apps/assets/views/admin_user.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# coding:utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import TemplateView, ListView
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from .. import forms
|
||||
from ..models import AdminUser, Node
|
||||
from ..hands import AdminUserRequiredMixin
|
||||
|
||||
__all__ = [
|
||||
'AdminUserCreateView', 'AdminUserDetailView',
|
||||
'AdminUserDeleteView', 'AdminUserListView',
|
||||
'AdminUserUpdateView', 'AdminUserAssetsView',
|
||||
]
|
||||
|
||||
|
||||
class AdminUserListView(AdminUserRequiredMixin, TemplateView):
|
||||
model = AdminUser
|
||||
template_name = 'assets/admin_user_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserCreateView(AdminUserRequiredMixin,
|
||||
SuccessMessageMixin,
|
||||
CreateView):
|
||||
model = AdminUser
|
||||
form_class = forms.AdminUserForm
|
||||
template_name = 'assets/admin_user_create_update.html'
|
||||
success_url = reverse_lazy('assets:admin-user-list')
|
||||
success_message = create_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create admin user')
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = AdminUser
|
||||
form_class = forms.AdminUserForm
|
||||
template_name = 'assets/admin_user_create_update.html'
|
||||
success_url = reverse_lazy('assets:admin-user-list')
|
||||
success_message = update_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update admin user'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdminUser
|
||||
template_name = 'assets/admin_user_detail.html'
|
||||
context_object_name = 'admin_user'
|
||||
object = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user detail'),
|
||||
'nodes': Node.objects.all()
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
template_name = 'assets/admin_user_assets.html'
|
||||
context_object_name = 'admin_user'
|
||||
object = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(queryset=AdminUser.objects.all())
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.object.asset_set.all()
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user detail'),
|
||||
"total_amount": len(self.queryset),
|
||||
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False])
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = AdminUser
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('assets:admin-user-list')
|
||||
|
||||
|
||||
342
apps/assets/views/asset.py
Normal file
342
apps/assets/views/asset.py
Normal file
@@ -0,0 +1,342 @@
|
||||
# coding:utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import csv
|
||||
import json
|
||||
import uuid
|
||||
import codecs
|
||||
import chardet
|
||||
from io import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import TemplateView, ListView, View
|
||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
|
||||
from common.mixins import JSONResponseMixin
|
||||
from common.utils import get_object_or_none, get_logger, is_uuid
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from .. import forms
|
||||
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
|
||||
from ..hands import AdminUserRequiredMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView',
|
||||
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
|
||||
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
|
||||
]
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AssetListView(AdminUserRequiredMixin, TemplateView):
|
||||
template_name = 'assets/asset_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
Node.root()
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Asset list'),
|
||||
'labels': Label.objects.all().order_by('name'),
|
||||
'nodes': Node.objects.all().order_by('-key'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserAssetListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'assets/user_asset_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'action': _('My assets'),
|
||||
'system_users': SystemUser.objects.all(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = Asset
|
||||
form_class = forms.AssetCreateForm
|
||||
template_name = 'assets/asset_create.html'
|
||||
success_url = reverse_lazy('assets:asset-list')
|
||||
|
||||
# def form_valid(self, form):
|
||||
# print("form valid")
|
||||
# asset = form.save()
|
||||
# asset.created_by = self.request.user.username or 'Admin'
|
||||
# asset.date_created = timezone.now()
|
||||
# asset.save()
|
||||
# return super().form_valid(form)
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class=form_class)
|
||||
node_id = self.request.GET.get("node_id")
|
||||
if node_id:
|
||||
node = get_object_or_none(Node, id=node_id)
|
||||
else:
|
||||
node = Node.root()
|
||||
form["nodes"].initial = node
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create asset'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return create_success_msg % ({"name": cleaned_data["hostname"]})
|
||||
|
||||
|
||||
# class AssetModalListView(AdminUserRequiredMixin, ListView):
|
||||
# paginate_by = settings.DISPLAY_PER_PAGE
|
||||
# model = Asset
|
||||
# context_object_name = 'asset_modal_list'
|
||||
# template_name = 'assets/_asset_list_modal.html'
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# assets = Asset.objects.all()
|
||||
# assets_id = self.request.GET.get('assets_id', '')
|
||||
# assets_id_list = [i for i in assets_id.split(',') if i.isdigit()]
|
||||
# context = {
|
||||
# 'all_assets': assets_id_list,
|
||||
# 'assets': assets
|
||||
# }
|
||||
# kwargs.update(context)
|
||||
# return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
|
||||
model = Asset
|
||||
form_class = forms.AssetBulkUpdateForm
|
||||
template_name = 'assets/asset_bulk_update.html'
|
||||
success_url = reverse_lazy('assets:asset-list')
|
||||
id_list = None
|
||||
form = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
assets_id = self.request.GET.get('assets_id', '')
|
||||
self.id_list = [i for i in assets_id.split(',')]
|
||||
|
||||
if kwargs.get('form'):
|
||||
self.form = kwargs['form']
|
||||
elif assets_id:
|
||||
self.form = self.form_class(
|
||||
initial={'assets': self.id_list}
|
||||
)
|
||||
else:
|
||||
self.form = self.form_class()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.form_class(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(self.success_url)
|
||||
else:
|
||||
return self.get(request, form=form, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Bulk update asset'),
|
||||
'form': self.form,
|
||||
'assets_selected': self.id_list,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Asset
|
||||
form_class = forms.AssetUpdateForm
|
||||
template_name = 'assets/asset_update.html'
|
||||
success_url = reverse_lazy('assets:asset-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update asset'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return update_success_msg % ({"name": cleaned_data["hostname"]})
|
||||
|
||||
|
||||
class AssetDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = Asset
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('assets:asset-list')
|
||||
|
||||
|
||||
class AssetDetailView(DetailView):
|
||||
model = Asset
|
||||
context_object_name = 'asset'
|
||||
template_name = 'assets/asset_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
nodes_remain = Node.objects.exclude(assets=self.object)
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Asset detail'),
|
||||
'nodes_remain': nodes_remain,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class AssetExportView(View):
|
||||
def get(self, request):
|
||||
spm = request.GET.get('spm', '')
|
||||
assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else []
|
||||
assets_id = cache.get(spm, assets_id_default)
|
||||
fields = [
|
||||
field for field in Asset._meta.fields
|
||||
if field.name not in [
|
||||
'date_created'
|
||||
]
|
||||
]
|
||||
filename = 'assets-{}.csv'.format(
|
||||
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
)
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
response.write(codecs.BOM_UTF8)
|
||||
assets = Asset.objects.filter(id__in=assets_id)
|
||||
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
|
||||
|
||||
header = [field.verbose_name for field in fields]
|
||||
writer.writerow(header)
|
||||
|
||||
for asset in assets:
|
||||
data = [getattr(asset, field.name) for field in fields]
|
||||
writer.writerow(data)
|
||||
return response
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
assets_id = json.loads(request.body).get('assets_id', [])
|
||||
assets_node_id = json.loads(request.body).get('node_id', None)
|
||||
except ValueError:
|
||||
return HttpResponse('Json object not valid', status=400)
|
||||
|
||||
if not assets_id and assets_node_id:
|
||||
assets_node = get_object_or_none(Node, id=assets_node_id)
|
||||
assets = assets_node.get_all_assets()
|
||||
for asset in assets:
|
||||
assets_id.append(asset.id)
|
||||
|
||||
spm = uuid.uuid4().hex
|
||||
cache.set(spm, assets_id, 300)
|
||||
url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm
|
||||
return JsonResponse({'redirect': url})
|
||||
|
||||
|
||||
class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
|
||||
form_class = forms.FileForm
|
||||
|
||||
def form_valid(self, form):
|
||||
node_id = self.request.GET.get("node_id")
|
||||
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
|
||||
f = form.cleaned_data['file']
|
||||
det_result = chardet.detect(f.read())
|
||||
f.seek(0) # reset file seek index
|
||||
|
||||
file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
|
||||
csv_file = StringIO(file_data)
|
||||
reader = csv.reader(csv_file)
|
||||
csv_data = [row for row in reader]
|
||||
fields = [
|
||||
field for field in Asset._meta.fields
|
||||
if field.name not in [
|
||||
'date_created'
|
||||
]
|
||||
]
|
||||
header_ = csv_data[0]
|
||||
mapping_reverse = {field.verbose_name: field.name for field in fields}
|
||||
attr = [mapping_reverse.get(n, None) for n in header_]
|
||||
if None in attr:
|
||||
data = {'valid': False,
|
||||
'msg': 'Must be same format as '
|
||||
'template or export file'}
|
||||
return self.render_json_response(data)
|
||||
|
||||
created, updated, failed = [], [], []
|
||||
assets = []
|
||||
for row in csv_data[1:]:
|
||||
if set(row) == {''}:
|
||||
continue
|
||||
|
||||
asset_dict_raw = dict(zip(attr, row))
|
||||
asset_dict = dict()
|
||||
for k, v in asset_dict_raw.items():
|
||||
v = v.strip()
|
||||
if k == 'is_active':
|
||||
v = False if v in ['False', 0, 'false'] else True
|
||||
elif k == 'admin_user':
|
||||
v = get_object_or_none(AdminUser, name=v)
|
||||
elif k in ['port', 'cpu_count', 'cpu_cores']:
|
||||
try:
|
||||
v = int(v)
|
||||
except ValueError:
|
||||
v = ''
|
||||
elif k == 'domain':
|
||||
v = get_object_or_none(Domain, name=v)
|
||||
|
||||
if v != '':
|
||||
asset_dict[k] = v
|
||||
|
||||
asset = get_object_or_none(Asset, id=asset_dict.pop('id', 0))
|
||||
if not asset:
|
||||
try:
|
||||
if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))):
|
||||
raise Exception(_('already exists'))
|
||||
with transaction.atomic():
|
||||
asset = Asset.objects.create(**asset_dict)
|
||||
if node:
|
||||
asset.nodes.set([node])
|
||||
created.append(asset_dict['hostname'])
|
||||
assets.append(asset)
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
|
||||
else:
|
||||
for k, v in asset_dict.items():
|
||||
if v != '':
|
||||
setattr(asset, k, v)
|
||||
try:
|
||||
asset.save()
|
||||
updated.append(asset_dict['hostname'])
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
|
||||
|
||||
data = {
|
||||
'created': created,
|
||||
'created_info': 'Created {}'.format(len(created)),
|
||||
'updated': updated,
|
||||
'updated_info': 'Updated {}'.format(len(updated)),
|
||||
'failed': failed,
|
||||
'failed_info': 'Failed {}'.format(len(failed)),
|
||||
'valid': True,
|
||||
'msg': 'Created: {}. Updated: {}, Error: {}'.format(
|
||||
len(created), len(updated), len(failed))
|
||||
}
|
||||
return self.render_json_response(data)
|
||||
|
||||
154
apps/assets/views/domain.py
Normal file
154
apps/assets/views/domain.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.views.generic import TemplateView, CreateView, \
|
||||
UpdateView, DeleteView, DetailView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse_lazy, reverse
|
||||
|
||||
from common.mixins import AdminUserRequiredMixin
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from common.utils import get_object_or_none
|
||||
from ..models import Domain, Gateway
|
||||
from ..forms import DomainForm, GatewayForm
|
||||
|
||||
|
||||
__all__ = (
|
||||
"DomainListView", "DomainCreateView", "DomainUpdateView",
|
||||
"DomainDetailView", "DomainDeleteView", "DomainGatewayListView",
|
||||
"DomainGatewayCreateView", 'DomainGatewayUpdateView',
|
||||
)
|
||||
|
||||
|
||||
class DomainListView(AdminUserRequiredMixin, TemplateView):
|
||||
template_name = 'assets/domain_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Domain list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainCreateView(AdminUserRequiredMixin, CreateView):
|
||||
model = Domain
|
||||
template_name = 'assets/domain_create_update.html'
|
||||
form_class = DomainForm
|
||||
success_url = reverse_lazy('assets:domain-list')
|
||||
success_message = create_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create domain'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
model = Domain
|
||||
template_name = 'assets/domain_create_update.html'
|
||||
form_class = DomainForm
|
||||
success_url = reverse_lazy('assets:domain-list')
|
||||
success_message = update_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update domain'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = Domain
|
||||
template_name = 'assets/domain_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Domain detail'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = Domain
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('assets:domain-list')
|
||||
|
||||
|
||||
class DomainGatewayListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateView):
|
||||
template_name = 'assets/domain_gateway_list.html'
|
||||
model = Domain
|
||||
object = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(queryset=self.model.objects.all())
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Domain gateway list'),
|
||||
'object': self.get_object()
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainGatewayCreateView(AdminUserRequiredMixin, CreateView):
|
||||
model = Gateway
|
||||
template_name = 'assets/gateway_create_update.html'
|
||||
form_class = GatewayForm
|
||||
success_message = create_success_msg
|
||||
|
||||
def get_success_url(self):
|
||||
domain = self.object.domain
|
||||
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class=form_class)
|
||||
domain_id = self.kwargs.get("pk")
|
||||
domain = get_object_or_none(Domain, id=domain_id)
|
||||
if domain:
|
||||
form['domain'].initial = domain
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create gateway'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
model = Gateway
|
||||
template_name = 'assets/gateway_create_update.html'
|
||||
form_class = GatewayForm
|
||||
success_message = update_success_msg
|
||||
|
||||
def get_success_url(self):
|
||||
domain = self.object.domain
|
||||
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
print(form.cleaned_data)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update gateway'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
72
apps/assets/views/label.py
Normal file
72
apps/assets/views/label.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.views.generic import TemplateView, CreateView, \
|
||||
UpdateView, DeleteView, DetailView
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from common.mixins import AdminUserRequiredMixin
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from ..models import Label
|
||||
from ..forms import LabelForm
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LabelListView", "LabelCreateView", "LabelUpdateView",
|
||||
"LabelDetailView", "LabelDeleteView",
|
||||
)
|
||||
|
||||
|
||||
class LabelListView(AdminUserRequiredMixin, TemplateView):
|
||||
template_name = 'assets/label_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Label list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class LabelCreateView(AdminUserRequiredMixin, CreateView):
|
||||
model = Label
|
||||
template_name = 'assets/label_create_update.html'
|
||||
form_class = LabelForm
|
||||
success_url = reverse_lazy('assets:label-list')
|
||||
success_message = create_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create label'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class LabelUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
model = Label
|
||||
template_name = 'assets/label_create_update.html'
|
||||
form_class = LabelForm
|
||||
success_url = reverse_lazy('assets:label-list')
|
||||
success_message = update_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update label'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class LabelDetailView(AdminUserRequiredMixin, DetailView):
|
||||
pass
|
||||
|
||||
|
||||
class LabelDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = Label
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('assets:label-list')
|
||||
99
apps/assets/views/system_user.py
Normal file
99
apps/assets/views/system_user.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from ..forms import SystemUserForm
|
||||
from ..models import SystemUser, Node
|
||||
from ..hands import AdminUserRequiredMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
'SystemUserCreateView', 'SystemUserUpdateView',
|
||||
'SystemUserDetailView', 'SystemUserDeleteView',
|
||||
'SystemUserAssetView', 'SystemUserListView',
|
||||
]
|
||||
|
||||
|
||||
class SystemUserListView(AdminUserRequiredMixin, TemplateView):
|
||||
template_name = 'assets/system_user_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('System user list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = SystemUser
|
||||
form_class = SystemUserForm
|
||||
template_name = 'assets/system_user_create.html'
|
||||
success_url = reverse_lazy('assets:system-user-list')
|
||||
success_message = create_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Create system user'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class SystemUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = SystemUser
|
||||
form_class = SystemUserForm
|
||||
template_name = 'assets/system_user_update.html'
|
||||
success_url = reverse_lazy('assets:system-user-list')
|
||||
success_message = update_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Update system user')
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class SystemUserDetailView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'assets/system_user_detail.html'
|
||||
context_object_name = 'system_user'
|
||||
model = SystemUser
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('System user detail'),
|
||||
'nodes_remain': Node.objects.exclude(systemuser=self.object)
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class SystemUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = SystemUser
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('assets:system-user-list')
|
||||
|
||||
|
||||
class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
|
||||
model = SystemUser
|
||||
template_name = 'assets/system_user_asset.html'
|
||||
context_object_name = 'system_user'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('assets'),
|
||||
'action': _('System user asset'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
14
apps/audits/api.py
Normal file
14
apps/audits/api.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from common.permissions import IsSuperUserOrAppUser
|
||||
from .models import FTPLog
|
||||
from .serializers import FTPLogSerializer
|
||||
|
||||
|
||||
class FTPLogViewSet(viewsets.ModelViewSet):
|
||||
queryset = FTPLog.objects.all()
|
||||
serializer_class = FTPLogSerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
5
apps/audits/apps.py
Normal file
5
apps/audits/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuditsConfig(AppConfig):
|
||||
name = 'audits'
|
||||
16
apps/audits/models.py
Normal file
16
apps/audits/models.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class FTPLog(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
user = models.CharField(max_length=128, verbose_name=_('User'))
|
||||
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
|
||||
system_user = models.CharField(max_length=128, verbose_name=_("System user"))
|
||||
operate = models.CharField(max_length=16, verbose_name=_("Operate"))
|
||||
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
||||
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
||||
date_start = models.DateTimeField(auto_now_add=True)
|
||||
13
apps/audits/serializers.py
Normal file
13
apps/audits/serializers.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import FTPLog
|
||||
|
||||
|
||||
class FTPLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FTPLog
|
||||
fields = '__all__'
|
||||
135
apps/audits/templates/audits/ftp_log_list.html
Normal file
135
apps/audits/templates/audits/ftp_log_list.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load terminal_tags %}
|
||||
{% load common_tags %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<style>
|
||||
#search_btn {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_left_head %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right form-inline">
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="user">
|
||||
<option value="">{% trans 'User' %}</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="asset">
|
||||
<option value="">{% trans 'Asset' %}</option>
|
||||
{% for a in asset_list %}
|
||||
<option value="{{ a }}" {% if a == asset %} selected {% endif %}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="system_user">
|
||||
<option value="">{% trans 'System user' %}</option>
|
||||
{% for su in system_user_list %}
|
||||
<option value="{{ su }}" {% if su == system_user %} selected {% endif %}>{{ su }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="filename" placeholder="{% trans 'Filename' %}" value="{{ filename }}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center"></th>
|
||||
{# <th class="text-center">{% trans 'ID' %}</th>#}
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'System user' %}</th>
|
||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||
<th class="text-center">{% trans 'Operate' %}</th>
|
||||
<th class="text-center">{% trans 'Filename' %}</th>
|
||||
<th class="text-center">{% trans 'Success' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
{# <th class="text-center">{% trans 'Action' %}</th>#}
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
|
||||
{# <td class="text-center">#}
|
||||
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
|
||||
{# </td>#}
|
||||
<td class="text-center">{{ object.user }}</td>
|
||||
<td class="text-center">{{ object.asset }}</td>
|
||||
<td class="text-center">{{ object.system_user }}</td>
|
||||
<td class="text-center">{{ object.remote_addr|default:"" }}</td>
|
||||
<td class="text-center">{{ object.operate }}</td>
|
||||
<td class="text-center">{{ object.filename }}</td>
|
||||
<td class="text-center">
|
||||
{% if object.is_success %}
|
||||
<i class="fa fa-check text-navy"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">{{ object.date_start }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_bottom_left %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('table').DataTable({
|
||||
"searching": false,
|
||||
"paging": false,
|
||||
"bInfo" : false,
|
||||
"order": []
|
||||
});
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth: true,
|
||||
width: "auto"
|
||||
});
|
||||
$('.input-daterange.input-group').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
18
apps/audits/urls/api_urls.py
Normal file
18
apps/audits/urls/api_urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = "audits"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'v1/ftp-log', api.FTPLogViewSet, 'ftp-log')
|
||||
|
||||
urlpatterns = [
|
||||
# url(r'^v1/celery/task/(?P<pk>[0-9a-zA-Z\-]{36})/log/$', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
14
apps/audits/urls/view_urls.py
Normal file
14
apps/audits/urls/view_urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.conf.urls import url
|
||||
from .. import views
|
||||
|
||||
__all__ = ["urlpatterns"]
|
||||
|
||||
app_name = "audits"
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ftp-log/$', views.FTPLogListView.as_view(), name='ftp-log-list'),
|
||||
]
|
||||
54
apps/audits/views.py
Normal file
54
apps/audits/views.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.mixins import AdminUserRequiredMixin, DatetimeSearchMixin
|
||||
|
||||
from .models import FTPLog
|
||||
|
||||
|
||||
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
model = FTPLog
|
||||
template_name = 'audits/ftp_log_list.html'
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
user = asset = system_user = filename = ''
|
||||
date_from = date_to = None
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()
|
||||
self.user = self.request.GET.get('user')
|
||||
self.asset = self.request.GET.get('asset')
|
||||
self.system_user = self.request.GET.get('system_user')
|
||||
self.filename = self.request.GET.get('filename', '')
|
||||
|
||||
filter_kwargs = dict()
|
||||
filter_kwargs['date_start__gt'] = self.date_from
|
||||
filter_kwargs['date_start__lt'] = self.date_to
|
||||
if self.user:
|
||||
filter_kwargs['user'] = self.user
|
||||
if self.asset:
|
||||
filter_kwargs['asset'] = self.asset
|
||||
if self.system_user:
|
||||
filter_kwargs['system_user'] = self.system_user
|
||||
if self.filename:
|
||||
filter_kwargs['filename__contains'] = self.filename
|
||||
if filter_kwargs:
|
||||
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-date_start')
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'user_list': FTPLog.objects.values_list('user', flat=True).distinct(),
|
||||
'asset_list': FTPLog.objects.values_list('asset', flat=True).distinct(),
|
||||
'system_user_list': FTPLog.objects.values_list('system_user', flat=True).distinct(),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user': self.user,
|
||||
'asset': self.asset,
|
||||
'system_user': self.system_user,
|
||||
'filename': self.filename,
|
||||
"app": _("Audits"),
|
||||
"action": _("FTP log"),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
76
apps/common/README.md
Normal file
76
apps/common/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Common app
|
||||
|
||||
Common app provide common view, function or others.
|
||||
|
||||
Common app shouldn't rely on other apps, because It may lead to cycle
|
||||
import.
|
||||
|
||||
If your want to implement some function or class, you should think
|
||||
whether other app use or not. If yes, You should make in common.
|
||||
|
||||
If the ability more relate to your app tightness, It's mean your app
|
||||
provide this ability, not common, You should write it on your app utils.
|
||||
|
||||
|
||||
|
||||
## Celery usage
|
||||
|
||||
|
||||
Jumpserver use celery to run task async. Using redis as the broker, so
|
||||
you should run a redis instance
|
||||
|
||||
#### Run redis
|
||||
|
||||
$ yum -y install redis
|
||||
|
||||
or
|
||||
|
||||
$ docker run -name jumpserver-redis -d -p 6379:6379 redis redis-server
|
||||
|
||||
|
||||
#### Write tasks in app_name/tasks.py
|
||||
|
||||
ops/tasks.py
|
||||
|
||||
```
|
||||
from __future__ import absolute_import
|
||||
|
||||
import time
|
||||
from celery import shared_task
|
||||
from common import celery_app
|
||||
|
||||
|
||||
@shared_task
|
||||
def longtime_add(x, y):
|
||||
print 'long time task begins'
|
||||
# sleep 5 seconds
|
||||
time.sleep(5)
|
||||
print 'long time task finished'
|
||||
return x + y
|
||||
|
||||
|
||||
@celery_app.task(name='hello-world')
|
||||
def hello():
|
||||
print 'hello world!'
|
||||
|
||||
```
|
||||
|
||||
#### Run celery in development
|
||||
|
||||
```
|
||||
$ cd apps
|
||||
$ celery -A common worker -l info
|
||||
```
|
||||
|
||||
#### Test using task
|
||||
|
||||
```
|
||||
$ ./manage.py shell
|
||||
>>> from ops.tasks import longtime_add
|
||||
>>> res = longtime_add.delay(1, 2)
|
||||
>>> res.get()
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
5
apps/common/__init__.py
Normal file
5
apps/common/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user