Compare commits
1413 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dddd2d896 | ||
|
|
f319c95c2b | ||
|
|
8e972b0907 | ||
|
|
395e400215 | ||
|
|
3685e3111f | ||
|
|
7bb1c75dc6 | ||
|
|
b20834929c | ||
|
|
181891757e | ||
|
|
b16feeae44 | ||
|
|
684e049f27 | ||
|
|
8cebd901b2 | ||
|
|
3c96beb8fb | ||
|
|
8a46459cf9 | ||
|
|
be5c3e9daa | ||
|
|
e44453877c | ||
|
|
f772a4ec56 | ||
|
|
44182ec683 | ||
|
|
b9ab13fa53 | ||
|
|
2ad6721c95 | ||
|
|
b7d0604e62 | ||
|
|
a7518b4b26 | ||
|
|
50613f5d3e | ||
|
|
f814767703 | ||
|
|
4af86d6456 | ||
|
|
f0a4f00c2d | ||
|
|
4321affddb | ||
|
|
926ed55b9b | ||
|
|
2ebf308565 | ||
|
|
1c5e736dce | ||
|
|
b591f9f5b7 | ||
|
|
9724882578 | ||
|
|
ddef2df101 | ||
|
|
8af69c4284 | ||
|
|
6ebe1ab467 | ||
|
|
24e4d9cf6d | ||
|
|
f35fa0aa58 | ||
|
|
4942f262f1 | ||
|
|
a20b1a973e | ||
|
|
eae5e00706 | ||
|
|
403762d862 | ||
|
|
5c92d4b454 | ||
|
|
38179b9d38 | ||
|
|
8f510dde5a | ||
|
|
be42d56e37 | ||
|
|
6294530fa3 | ||
|
|
c5c8f5fab1 | ||
|
|
3d41d79078 | ||
|
|
3005061a11 | ||
|
|
65ea46f457 | ||
|
|
eca8f32570 | ||
|
|
8d1ef19c61 | ||
|
|
71d87d866b | ||
|
|
c4f88bdce7 | ||
|
|
f722a115b1 | ||
|
|
1583beea7b | ||
|
|
5b388c587b | ||
|
|
e254923167 | ||
|
|
b0dbdd7803 | ||
|
|
aa6ebe0122 | ||
|
|
c5f179bab8 | ||
|
|
e65cb86638 | ||
|
|
a349998640 | ||
|
|
43f60610b8 | ||
|
|
46d042087a | ||
|
|
ee214727f6 | ||
|
|
b4c1ec55ec | ||
|
|
0fdd54f710 | ||
|
|
4f0cdeaec0 | ||
|
|
e5cc38857c | ||
|
|
fe4b9d71c0 | ||
|
|
5c1181e40e | ||
|
|
8b71832bc2 | ||
|
|
8412ed6065 | ||
|
|
207f6cdc7c | ||
|
|
b0b51f5730 | ||
|
|
def6833ef0 | ||
|
|
c528dd3de1 | ||
|
|
544270e35d | ||
|
|
657e029fee | ||
|
|
49469d7689 | ||
|
|
4f0dd452c8 | ||
|
|
3f741eab11 | ||
|
|
190368788f | ||
|
|
8306a3f566 | ||
|
|
988c134c09 | ||
|
|
af0a4d578b | ||
|
|
9bc0abc831 | ||
|
|
41410e99e7 | ||
|
|
deae04d5ff | ||
|
|
7d6eeffd66 | ||
|
|
629858e095 | ||
|
|
dfdb628347 | ||
|
|
6e48b28fc9 | ||
|
|
3ba450e837 | ||
|
|
688ed93500 | ||
|
|
7268ba20a2 | ||
|
|
63d9e73098 | ||
|
|
564c048f90 | ||
|
|
5f801c74d5 | ||
|
|
b405fbc09a | ||
|
|
7a64c2eb49 | ||
|
|
c93cbac3b1 | ||
|
|
8b0f67b8a6 | ||
|
|
0d96129f2d | ||
|
|
54ee12d2b3 | ||
|
|
92fc042103 | ||
|
|
9bb7016fa7 | ||
|
|
3ad56feafb | ||
|
|
14d59c3dec | ||
|
|
443f419770 | ||
|
|
ddbb58755e | ||
|
|
524283b9ff | ||
|
|
fb178d2944 | ||
|
|
52f4ad9403 | ||
|
|
ba0c08ef1f | ||
|
|
9e19b1e04c | ||
|
|
b2118201b1 | ||
|
|
b4346aa056 | ||
|
|
b599f05aab | ||
|
|
93d78a0200 | ||
|
|
449957b2eb | ||
|
|
0a6d44bad3 | ||
|
|
17ceaaa503 | ||
|
|
d70803b416 | ||
|
|
aa414d4702 | ||
|
|
f24e1b91ea | ||
|
|
1df8163090 | ||
|
|
659ddf6a45 | ||
|
|
e110068da4 | ||
|
|
c943f6f936 | ||
|
|
cb1fe7fe54 | ||
|
|
593f1f63cc | ||
|
|
66aa70cf75 | ||
|
|
304be99067 | ||
|
|
9a01ec35f4 | ||
|
|
bfa5b4fba5 | ||
|
|
d2f63ef353 | ||
|
|
50f334425e | ||
|
|
f78212073c | ||
|
|
5c655f5a82 | ||
|
|
6a6446bfcb | ||
|
|
b60a3a5e50 | ||
|
|
02ccbab8e5 | ||
|
|
023ff3f964 | ||
|
|
7c5e8df3b8 | ||
|
|
56fdab260b | ||
|
|
7cce49dc1a | ||
|
|
2dfaafb20b | ||
|
|
6138a5bf54 | ||
|
|
828c67cc00 | ||
|
|
e70cd44e18 | ||
|
|
efa5ac5edd | ||
|
|
788b11e759 | ||
|
|
d049d7a61f | ||
|
|
075c833b58 | ||
|
|
e9309c2a96 | ||
|
|
a592d2b397 | ||
|
|
3ad1805ac0 | ||
|
|
dbc2bab698 | ||
|
|
79eec5c299 | ||
|
|
7754b0c575 | ||
|
|
be4289ce76 | ||
|
|
67f5226270 | ||
|
|
b6d77c581b | ||
|
|
d84bf47d04 | ||
|
|
aba3a7bb9e | ||
|
|
6281736d89 | ||
|
|
94d96f89d3 | ||
|
|
4b55f9dead | ||
|
|
5c6dce94df | ||
|
|
f7d8f9c7f5 | ||
|
|
053df24f9c | ||
|
|
1dc470e434 | ||
|
|
cfd8773267 | ||
|
|
67045cf6c1 | ||
|
|
ddfb9e7239 | ||
|
|
9f6eed5472 | ||
|
|
15a1e2ebcb | ||
|
|
fcfe450b07 | ||
|
|
a69bbb3bc9 | ||
|
|
6d2559cfc1 | ||
|
|
b3a62615f3 | ||
|
|
57f5cca1cb | ||
|
|
6b9851f540 | ||
|
|
36fd203a88 | ||
|
|
3f5cb5d61c | ||
|
|
862fc6a946 | ||
|
|
92c386ac0e | ||
|
|
98a11a3645 | ||
|
|
62be0ed936 | ||
|
|
b7de73fd8a | ||
|
|
e2413f1af2 | ||
|
|
0e77d575c4 | ||
|
|
ba42c5e367 | ||
|
|
6a06734192 | ||
|
|
5e26a406b7 | ||
|
|
b6dd03138d | ||
|
|
cf03ee03ee | ||
|
|
0e665b6bf0 | ||
|
|
e3d0de7313 | ||
|
|
bcf3a543a1 | ||
|
|
b27f17c74a | ||
|
|
75d864771e | ||
|
|
6420060f2a | ||
|
|
c149ae71b9 | ||
|
|
3a49dd034c | ||
|
|
b26d7e82e3 | ||
|
|
415abdf0ce | ||
|
|
f7f6f6ecb2 | ||
|
|
43d54f134a | ||
|
|
0d2606a13b | ||
|
|
1deb10dc88 | ||
|
|
1236d55544 | ||
|
|
ecccf39455 | ||
|
|
8e0316825a | ||
|
|
aa45fa87af | ||
|
|
71e78bd0c5 | ||
|
|
4766477c58 | ||
|
|
d97e49ff2b | ||
|
|
6b9d775cb9 | ||
|
|
e521f580d7 | ||
|
|
25e7cf7db0 | ||
|
|
0cab33787d | ||
|
|
bc6faf817f | ||
|
|
d46ae55863 | ||
|
|
bbd900ab25 | ||
|
|
129ae93e2b | ||
|
|
44dd59fa3f | ||
|
|
ec4e7559b0 | ||
|
|
dce40611cf | ||
|
|
e71b8546f9 | ||
|
|
f827348467 | ||
|
|
f3978343db | ||
|
|
2654a7ea70 | ||
|
|
1068bf4ef7 | ||
|
|
e7fccc97cc | ||
|
|
733e289852 | ||
|
|
29d71a104c | ||
|
|
05200420ad | ||
|
|
eb762d4bfd | ||
|
|
58ace9eda1 | ||
|
|
eeb2623be0 | ||
|
|
cfa242c2fe | ||
|
|
ec0441ccc2 | ||
|
|
ae2782a8fe | ||
|
|
58ff570251 | ||
|
|
7b554b12c7 | ||
|
|
58f7603d4f | ||
|
|
8895994c54 | ||
|
|
de8f7e36d5 | ||
|
|
88d7a50265 | ||
|
|
21e19fc7e5 | ||
|
|
faf4935a69 | ||
|
|
71a1f9d74a | ||
|
|
bd8d523e10 | ||
|
|
60cae0e3ac | ||
|
|
5a342ac012 | ||
|
|
bb8767dfc3 | ||
|
|
fcb2779c15 | ||
|
|
77dd6c1f61 | ||
|
|
8118eef300 | ||
|
|
802d1489fe | ||
|
|
443a029185 | ||
|
|
4ee508fdd0 | ||
|
|
aa5608f7e8 | ||
|
|
cc472b4613 | ||
|
|
764b945ddc | ||
|
|
fd2206ce4c | ||
|
|
48c0ac9f00 | ||
|
|
84eb4fe9ed | ||
|
|
4a5428812c | ||
|
|
023f98a89d | ||
|
|
66893dd0c1 | ||
|
|
25a6666e35 | ||
|
|
19d75309b5 | ||
|
|
11110d65c1 | ||
|
|
a348f58fe2 | ||
|
|
13851dd976 | ||
|
|
2ec37c5da9 | ||
|
|
8c127160de | ||
|
|
2af820de9a | ||
|
|
55fb0bb3a0 | ||
|
|
9f9ecc521f | ||
|
|
dfd01df5ba | ||
|
|
474090698c | ||
|
|
6b71cdeea4 | ||
|
|
581e974236 | ||
|
|
ba3c3a42ce | ||
|
|
c8bc5671c5 | ||
|
|
ff9401a040 | ||
|
|
5e1bc1989f | ||
|
|
a1dc91cd7d | ||
|
|
99f2772bb3 | ||
|
|
e5d0e42655 | ||
|
|
2c914cc374 | ||
|
|
9bceb62381 | ||
|
|
de7518a800 | ||
|
|
304fb63453 | ||
|
|
0f7ef60ca0 | ||
|
|
07c74e4641 | ||
|
|
de7f325cfb | ||
|
|
42cdf70cb4 | ||
|
|
6beb6be131 | ||
|
|
fa4fc2a708 | ||
|
|
2db9758260 | ||
|
|
715982e40a | ||
|
|
d00cd4453a | ||
|
|
429c08c24a | ||
|
|
6a71490e20 | ||
|
|
9bceda0646 | ||
|
|
a1027a6773 | ||
|
|
302d4b75f9 | ||
|
|
5f6ee0e883 | ||
|
|
27f9720de1 | ||
|
|
22aa3fdbbc | ||
|
|
069ecdd33f | ||
|
|
dd545ae933 | ||
|
|
6650b705c4 | ||
|
|
59b0350289 | ||
|
|
1ad159f820 | ||
|
|
0bf42190e9 | ||
|
|
d2fa836232 | ||
|
|
c387774093 | ||
|
|
e99736ba3c | ||
|
|
16cb54fcc9 | ||
|
|
5aa15c51ec | ||
|
|
a8aedd9cf3 | ||
|
|
b851b632bc | ||
|
|
541e07fb65 | ||
|
|
6ad16a897d | ||
|
|
72f1053a93 | ||
|
|
fb15a2762c | ||
|
|
9165248b91 | ||
|
|
add18b29db | ||
|
|
1971653548 | ||
|
|
392cd64d7b | ||
|
|
b5affbb7c8 | ||
|
|
71d1206277 | ||
|
|
26e6a8c409 | ||
|
|
eb54fae11a | ||
|
|
ee773e5966 | ||
|
|
7218ccdba8 | ||
|
|
332400e48a | ||
|
|
ad1a5d3702 | ||
|
|
3006b4184d | ||
|
|
84eb84a080 | ||
|
|
60beea548b | ||
|
|
5f9c149e59 | ||
|
|
53367c6f04 | ||
|
|
d7f817ee44 | ||
|
|
d33a87da54 | ||
|
|
3aebfb12b7 | ||
|
|
1d6c55ffa6 | ||
|
|
5e7080aac3 | ||
|
|
fad739bc01 | ||
|
|
c6b7f23884 | ||
|
|
a6f7e446de | ||
|
|
89d95d3ae1 | ||
|
|
764208698f | ||
|
|
57129cf934 | ||
|
|
aae1a842d5 | ||
|
|
623f35aec7 | ||
|
|
870bf842cf | ||
|
|
07f2d7dd5c | ||
|
|
f223f2edc5 | ||
|
|
e848a9a577 | ||
|
|
7569d98e07 | ||
|
|
596dee2f24 | ||
|
|
9970403964 | ||
|
|
07a88ae00d | ||
|
|
5475b4d287 | ||
|
|
6631dcfd3e | ||
|
|
0dd3f337f3 | ||
|
|
8eb27b5875 | ||
|
|
2d1863031c | ||
|
|
9feb76ca81 | ||
|
|
993e8f4ab3 | ||
|
|
e08ae95d4f | ||
|
|
15359e8846 | ||
|
|
d1457b312b | ||
|
|
c9dd2af196 | ||
|
|
564ef4e688 | ||
|
|
a33e6e8bb5 | ||
|
|
cf34f33f04 | ||
|
|
827cfe4e8f | ||
|
|
2ce1c2383c | ||
|
|
6fc0a665ae | ||
|
|
4f16d01263 | ||
|
|
67cc37354a | ||
|
|
e388243ef4 | ||
|
|
3dc92763c7 | ||
|
|
dfe97dd466 | ||
|
|
2803cee29b | ||
|
|
3a03020e54 | ||
|
|
64443cc703 | ||
|
|
4d1aa6ed18 | ||
|
|
84837e88d2 | ||
|
|
ff49c936ea | ||
|
|
e6e0901329 | ||
|
|
23b6284b51 | ||
|
|
33dfbcbe32 | ||
|
|
700c23d537 | ||
|
|
369fac9e38 | ||
|
|
2229eb1167 | ||
|
|
a3dec841b6 | ||
|
|
b17620bdb6 | ||
|
|
f39cd5ae2f | ||
|
|
83a19e005b | ||
|
|
a9dd01b0c8 | ||
|
|
eb59afa1d1 | ||
|
|
2adcfce9d0 | ||
|
|
314ab9b304 | ||
|
|
8576fb82c7 | ||
|
|
0f95a6bb2f | ||
|
|
ad5104567d | ||
|
|
ece68ba1d5 | ||
|
|
acccd3a586 | ||
|
|
8ebef1c1ca | ||
|
|
28abc0d5ed | ||
|
|
1efe25d3ec | ||
|
|
c40e4f8e4b | ||
|
|
baca84092d | ||
|
|
346d4da059 | ||
|
|
ade64d6c0a | ||
|
|
8204bdfc5f | ||
|
|
1a9bb3e986 | ||
|
|
49356479e5 | ||
|
|
c44e9a7292 | ||
|
|
21771a593f | ||
|
|
84458dfc4c | ||
|
|
5835632dab | ||
|
|
67aa7229ef | ||
|
|
b72dc3ed3a | ||
|
|
0f93d4a5bd | ||
|
|
106320b035 | ||
|
|
63951705cd | ||
|
|
a8d56921d5 | ||
|
|
10bc133cf1 | ||
|
|
adeb5b35c9 | ||
|
|
589ff46ea5 | ||
|
|
656fcb9fe7 | ||
|
|
1cb9353006 | ||
|
|
57bf16ba07 | ||
|
|
659846ed88 | ||
|
|
25894044e0 | ||
|
|
e7a0826beb | ||
|
|
1f7ddee23b | ||
|
|
7e186730db | ||
|
|
6713a50208 | ||
|
|
7c9d8fcfec | ||
|
|
33bfc8cfe8 | ||
|
|
ca735bc14a | ||
|
|
4ba748a18b | ||
|
|
f1845106f8 | ||
|
|
67e7156c4b | ||
|
|
4a476adebf | ||
|
|
918798f8cc | ||
|
|
5a3f868866 | ||
|
|
feea2c6396 | ||
|
|
707b4c46d9 | ||
|
|
89ca39fc2b | ||
|
|
204281b12d | ||
|
|
a8538a7e95 | ||
|
|
dee1b471e9 | ||
|
|
aa04e9b01f | ||
|
|
350f0dc604 | ||
|
|
6021f2efd6 | ||
|
|
51838ec25a | ||
|
|
54768a121e | ||
|
|
8ff72cdca3 | ||
|
|
2cb53ad06b | ||
|
|
b8349de31d | ||
|
|
d7e11af7f8 | ||
|
|
dd8d39e698 | ||
|
|
afb1316daa | ||
|
|
04d7017536 | ||
|
|
6a1c75b060 | ||
|
|
5c94611f3b | ||
|
|
4e5676e80f | ||
|
|
c96d688a9c | ||
|
|
804242e9a5 | ||
|
|
0ec9760b17 | ||
|
|
d481ae3da4 | ||
|
|
4742c14fc1 | ||
|
|
509b0d501b | ||
|
|
d4c9b04d4e | ||
|
|
16fb4d331b | ||
|
|
e9e5bf31a7 | ||
|
|
221418120e | ||
|
|
46f852e26e | ||
|
|
4234cf0a31 | ||
|
|
7f3daea648 | ||
|
|
2eb16c82f4 | ||
|
|
e00b2ce591 | ||
|
|
d71e1311ca | ||
|
|
2cf16963e3 | ||
|
|
10bf7b7fb4 | ||
|
|
182c85a228 | ||
|
|
94b1988b90 | ||
|
|
6f7e62e9a0 | ||
|
|
aa7076af04 | ||
|
|
c928e8f0d4 | ||
|
|
5c6b106f68 | ||
|
|
d45bcea1ff | ||
|
|
6ff2dc79f8 | ||
|
|
b752329987 | ||
|
|
f21465335a | ||
|
|
0801adfc4b | ||
|
|
5bee8052d5 | ||
|
|
68dca5dfef | ||
|
|
3f51dd1d2f | ||
|
|
7f80889d77 | ||
|
|
efc61c0222 | ||
|
|
6fc0a05d34 | ||
|
|
a9be872d7a | ||
|
|
6ca85f099e | ||
|
|
86ff677b8a | ||
|
|
35e295df86 | ||
|
|
cd4d301790 | ||
|
|
93bb329c3d | ||
|
|
7c1e0f2c30 | ||
|
|
b57f471f44 | ||
|
|
252a9a2ed6 | ||
|
|
7258d4d787 | ||
|
|
75522fa295 | ||
|
|
4ba8f41d95 | ||
|
|
f326f8e4de | ||
|
|
f863dc058e | ||
|
|
20891db251 | ||
|
|
f1d05f1342 | ||
|
|
8dd636b0eb | ||
|
|
6b5bda8ee1 | ||
|
|
ddc5597157 | ||
|
|
ae112c7257 | ||
|
|
c22f10f96a | ||
|
|
18d10c9bec | ||
|
|
890e430cb7 | ||
|
|
dadc3d4cd7 | ||
|
|
d98b4d7320 | ||
|
|
340f532238 | ||
|
|
7669f68e7c | ||
|
|
3557e5514f | ||
|
|
a9f09b7614 | ||
|
|
845b9e4568 | ||
|
|
24a6092dcf | ||
|
|
195ae7d8b1 | ||
|
|
a5c6ea7ffc | ||
|
|
eb7a4ac29f | ||
|
|
508ef73fde | ||
|
|
838d6d8076 | ||
|
|
762c3159b8 | ||
|
|
7a88a06bcf | ||
|
|
0b1e3d7de5 | ||
|
|
9a83c73f21 | ||
|
|
aa50c7b268 | ||
|
|
179a5a80f4 | ||
|
|
0ddae527ef | ||
|
|
ee7a46de26 | ||
|
|
95522fda74 | ||
|
|
e58881c2bd | ||
|
|
36a902a44e | ||
|
|
16b74549a2 | ||
|
|
da7ededfb1 | ||
|
|
790bb08718 | ||
|
|
e6765f421f | ||
|
|
7e8f1fe904 | ||
|
|
eacce4578a | ||
|
|
07b2543972 | ||
|
|
d1c3fc8493 | ||
|
|
f453b16010 | ||
|
|
05151d8978 | ||
|
|
8218e1acc3 | ||
|
|
30212fc89a | ||
|
|
b31c13fcae | ||
|
|
6b95fc6f1d | ||
|
|
369cf17eb2 | ||
|
|
4dd8f512cc | ||
|
|
26cfec7d80 | ||
|
|
67a87ccf00 | ||
|
|
667cebcf94 | ||
|
|
bc1747ca1c | ||
|
|
945d8647bf | ||
|
|
dfe2e94627 | ||
|
|
09a5591eec | ||
|
|
f2bf06a0ba | ||
|
|
eedad4ab1c | ||
|
|
336a62ab29 | ||
|
|
b5603a5233 | ||
|
|
73890f553c | ||
|
|
f6243b8968 | ||
|
|
3770dc74d4 | ||
|
|
45f4e947c5 | ||
|
|
9928d7c6e1 | ||
|
|
bf776eeb2b | ||
|
|
ae7c0e9195 | ||
|
|
e90b640602 | ||
|
|
ba7529d3f5 | ||
|
|
34667f252e | ||
|
|
d18bddcb7b | ||
|
|
96dff49d33 | ||
|
|
b389728338 | ||
|
|
cdc7da86f3 | ||
|
|
4745cc0378 | ||
|
|
434f132479 | ||
|
|
fb0f31ffc7 | ||
|
|
bb1d73c0ae | ||
|
|
0e823d1191 | ||
|
|
48f4199ff3 | ||
|
|
eaf379587b | ||
|
|
672446b7d1 | ||
|
|
dfe52c1b07 | ||
|
|
d63df03ad8 | ||
|
|
aba4f9f2ce | ||
|
|
ac5c1e7803 | ||
|
|
d521dbf50e | ||
|
|
f210ed3e6a | ||
|
|
df3cac4ea6 | ||
|
|
f778c5175b | ||
|
|
6c66ff28dd | ||
|
|
d5b6ec702b | ||
|
|
c62a5fcef2 | ||
|
|
59c47e9200 | ||
|
|
4ba44d8932 | ||
|
|
27dae05e1b | ||
|
|
a251ae9b90 | ||
|
|
7e960b2bde | ||
|
|
5df4825158 | ||
|
|
8984d06d93 | ||
|
|
eed7aac047 | ||
|
|
54b068de4a | ||
|
|
f0f33b00b6 | ||
|
|
1043405088 | ||
|
|
0131b10805 | ||
|
|
a19b441f62 | ||
|
|
28edc31d43 | ||
|
|
0f9872a818 | ||
|
|
76ce4296f3 | ||
|
|
3dd2671380 | ||
|
|
298ca31332 | ||
|
|
8f911aa6b9 | ||
|
|
82a5c7d9b1 | ||
|
|
7f013dcdba | ||
|
|
68e2e16076 | ||
|
|
ea23c763c9 | ||
|
|
5dcecb3206 | ||
|
|
5bd48e2d0e | ||
|
|
afd0a02589 | ||
|
|
2379192d53 | ||
|
|
a6489290c8 | ||
|
|
5f74c43415 | ||
|
|
aa8b84a302 | ||
|
|
b987d041b0 | ||
|
|
b62e37307e | ||
|
|
61a59aa6ac | ||
|
|
f79ec27f1d | ||
|
|
b993fe380f | ||
|
|
d974b5f55f | ||
|
|
f21ae93197 | ||
|
|
342ff18be8 | ||
|
|
a8236f69bf | ||
|
|
ab15a2448d | ||
|
|
6ff4d8f558 | ||
|
|
bb04ba528c | ||
|
|
b94a795189 | ||
|
|
9968184733 | ||
|
|
1be6f8f87a | ||
|
|
426821cceb | ||
|
|
4fec0deaf7 | ||
|
|
144ac5b6ce | ||
|
|
97c73786fa | ||
|
|
82e59d7da0 | ||
|
|
b2c10de6af | ||
|
|
d72029c2c6 | ||
|
|
17b9987063 | ||
|
|
fde07da2b7 | ||
|
|
c23bc29511 | ||
|
|
714cad2a52 | ||
|
|
357d5d2fde | ||
|
|
d477cce901 | ||
|
|
eb6af52ad1 | ||
|
|
aae75023a7 | ||
|
|
41dcd4f458 | ||
|
|
4651ae4495 | ||
|
|
ed61e0b0fc | ||
|
|
1eefc6fbf4 | ||
|
|
09ebf2cea2 | ||
|
|
b3b0c4cd65 | ||
|
|
f4b7924e8f | ||
|
|
ea68d38b82 | ||
|
|
dfbaa71132 | ||
|
|
6c328deb08 | ||
|
|
add564d5bf | ||
|
|
fa94acb426 | ||
|
|
6827468f13 | ||
|
|
53fd43868f | ||
|
|
9ced7561c5 | ||
|
|
31d55d3425 | ||
|
|
171d2a5bb9 | ||
|
|
c5d05c1205 | ||
|
|
2973e0559a | ||
|
|
ec27288dcf | ||
|
|
f92e5c7093 | ||
|
|
7c67155c49 | ||
|
|
b102cd4652 | ||
|
|
67f9a48c37 | ||
|
|
a0c8a1ee65 | ||
|
|
7e7d272b06 | ||
|
|
3c642240ae | ||
|
|
b5157fcaf1 | ||
|
|
d1cb42f1bc | ||
|
|
84cde1a16a | ||
|
|
877f5db1ce | ||
|
|
787164e245 | ||
|
|
d77fc5e7c5 | ||
|
|
cca39a67d6 | ||
|
|
a6c9a0431a | ||
|
|
729a80a639 | ||
|
|
31cb3001f6 | ||
|
|
5d0f54a329 | ||
|
|
c8c3f5b5b7 | ||
|
|
ba473ed75a | ||
|
|
7236fd59f8 | ||
|
|
9471e8f1fd | ||
|
|
a2d39b51bb | ||
|
|
2920934b55 | ||
|
|
3f709d448e | ||
|
|
b79f66183f | ||
|
|
8672f57e55 | ||
|
|
1e99c82351 | ||
|
|
1a2ff851f3 | ||
|
|
f1c27c3959 | ||
|
|
b30dac0f15 | ||
|
|
cc79e5cdaf | ||
|
|
d9a3b2f2cb | ||
|
|
479b528d09 | ||
|
|
461fb84fb9 | ||
|
|
bd7685e3fa | ||
|
|
cd98cb64b3 | ||
|
|
0f32a3ec24 | ||
|
|
ca446cac87 | ||
|
|
6ea907ffda | ||
|
|
5287baa70d | ||
|
|
25935fec84 | ||
|
|
e855a063ff | ||
|
|
c726b8c9f0 | ||
|
|
13cb99290e | ||
|
|
cea9413fd1 | ||
|
|
1432853b39 | ||
|
|
6d6c2b86e8 | ||
|
|
77b1d964b5 | ||
|
|
549936fc09 | ||
|
|
c9c32f09c5 | ||
|
|
77f7778d4a | ||
|
|
84b6be9364 | ||
|
|
1e43b55804 | ||
|
|
ba9bdaae0a | ||
|
|
7dfd7bde8e | ||
|
|
5e6c4161d0 | ||
|
|
d75d56dfc9 | ||
|
|
1d9d350091 | ||
|
|
5744053c6f | ||
|
|
65589b6ca2 | ||
|
|
e03a9d1137 | ||
|
|
29f80f2276 | ||
|
|
a9b74aa69b | ||
|
|
63ebfd3210 | ||
|
|
87fa5ff7a6 | ||
|
|
b686b53a9c | ||
|
|
258261dc64 | ||
|
|
9af5c9ead9 | ||
|
|
382654188c | ||
|
|
fa1df082b7 | ||
|
|
5c227d8f80 | ||
|
|
81dabdbfb7 | ||
|
|
91f89f5a33 | ||
|
|
9f92746aa0 | ||
|
|
5d6e6f9441 | ||
|
|
01395a2726 | ||
|
|
465d75c65d | ||
|
|
4634f8927e | ||
|
|
74a287f9fe | ||
|
|
7ff6c79835 | ||
|
|
3629982237 | ||
|
|
ddb610f1bc | ||
|
|
f899905d27 | ||
|
|
3e4531b5c5 | ||
|
|
a9e189e51d | ||
|
|
58ba08a8f3 | ||
|
|
9078ff27d8 | ||
|
|
6f43e61c24 | ||
|
|
4be0d3f212 | ||
|
|
00e47e5a27 | ||
|
|
152e145b32 | ||
|
|
54e55e8f57 | ||
|
|
05b8707f9e | ||
|
|
543e952023 | ||
|
|
6e5f40ea06 | ||
|
|
bbafb0be87 | ||
|
|
1c9c5232fe | ||
|
|
598d79a502 | ||
|
|
37d8360b77 | ||
|
|
82d9ca3317 | ||
|
|
4e4238d486 | ||
|
|
c77dbe44dc | ||
|
|
e03737f15f | ||
|
|
a02629bcd7 | ||
|
|
6c3fc23d78 | ||
|
|
0fe40f9ccb | ||
|
|
9bd7c8edd1 | ||
|
|
83ba480863 | ||
|
|
f158ea25e9 | ||
|
|
0227519eab | ||
|
|
616a9685fa | ||
|
|
fe61b01320 | ||
|
|
7b25144311 | ||
|
|
9d42fbbdd7 | ||
|
|
39ac5b088b | ||
|
|
c14ffd08a0 | ||
|
|
6e1239340b | ||
|
|
a297dc8b3b | ||
|
|
8d4ecc0898 | ||
|
|
eae9c04429 | ||
|
|
a41c48a9c5 | ||
|
|
ff2a94bd9b | ||
|
|
4a1f5558b8 | ||
|
|
608db9889f | ||
|
|
012b697337 | ||
|
|
0580506cf3 | ||
|
|
ff4ab9b661 | ||
|
|
b7ce5fdd3e | ||
|
|
a11e617322 | ||
|
|
d0beac7e2b | ||
|
|
9db497092f | ||
|
|
8eb91c08aa | ||
|
|
ded5437522 | ||
|
|
9348657951 | ||
|
|
bca85933f7 | ||
|
|
c32bb35f1c | ||
|
|
4b84062d62 | ||
|
|
d6d0f8fa17 | ||
|
|
dd72c875d3 | ||
|
|
1a1df50300 | ||
|
|
53cbb527b4 | ||
|
|
8b87b2717e | ||
|
|
1007d6dac7 | ||
|
|
6799fac120 | ||
|
|
558e6288ca | ||
|
|
d9cb73291b | ||
|
|
d0f7be3ac3 | ||
|
|
331e16d3ca | ||
|
|
0db246c311 | ||
|
|
94dc62ff58 | ||
|
|
e68ecf6844 | ||
|
|
5167b0a8c6 | ||
|
|
77e3d3786d | ||
|
|
708d4d39bc | ||
|
|
2a8cda2a1e | ||
|
|
8d783840ad | ||
|
|
abe39d5790 | ||
|
|
d7868e9e5a | ||
|
|
7b84e36e15 | ||
|
|
6cab6d69d8 | ||
|
|
87846d7aef | ||
|
|
2557769c6a | ||
|
|
48375f3878 | ||
|
|
176c85d8c1 | ||
|
|
17cad71ede | ||
|
|
e8bf9d4e6f | ||
|
|
7bdd2038ef | ||
|
|
e9f6e7943a | ||
|
|
e74ba387ab | ||
|
|
27c79e5b99 | ||
|
|
8170d5ea73 | ||
|
|
196f73705d | ||
|
|
ad0bbf5248 | ||
|
|
4cae9cd90d | ||
|
|
be7bc55a76 | ||
|
|
684b545e8f | ||
|
|
7835cc3b10 | ||
|
|
f8706b51e8 | ||
|
|
d97f8fd5da | ||
|
|
f8fa87441e | ||
|
|
d42537814a | ||
|
|
792421b0e2 | ||
|
|
72d55a010b | ||
|
|
880d8258ce | ||
|
|
b79bf82efb | ||
|
|
b3118b6253 | ||
|
|
ba172e2e25 | ||
|
|
892d53abeb | ||
|
|
5cbaa1ce98 | ||
|
|
7b35d9ad2e | ||
|
|
8462de7911 | ||
|
|
8721f44298 | ||
|
|
c7a2d69afa | ||
|
|
0453d81e7a | ||
|
|
501c04ac2b | ||
|
|
0ef4e9a5c3 | ||
|
|
129c50e598 | ||
|
|
3e276fc2ac | ||
|
|
658d5e05ae | ||
|
|
4e7d5d476e | ||
|
|
6a55ca20f3 | ||
|
|
c56c537f7f | ||
|
|
fd7d776121 | ||
|
|
1af28190d8 | ||
|
|
6b305be567 | ||
|
|
3bf70513b7 | ||
|
|
7e64404654 | ||
|
|
e1b5226f34 | ||
|
|
0d7128ad31 | ||
|
|
5778626087 | ||
|
|
3ff48756ed | ||
|
|
0ce9a6eeba | ||
|
|
ad527b4aed | ||
|
|
6633bb452e | ||
|
|
efeb0b4feb | ||
|
|
8cc11fc102 | ||
|
|
ee6a167220 | ||
|
|
8d4ad3c405 | ||
|
|
072fbf4d60 | ||
|
|
727c41c283 | ||
|
|
e2266838b6 | ||
|
|
775762d615 | ||
|
|
900c3008cb | ||
|
|
09379213a6 | ||
|
|
ceb97048e3 | ||
|
|
4561515517 | ||
|
|
a7b285759f | ||
|
|
b4531b2a12 | ||
|
|
9e1d261c76 | ||
|
|
e35fa15cd2 | ||
|
|
dbd1f0d4f9 | ||
|
|
9ade78b703 | ||
|
|
f20e244b5f | ||
|
|
0989308b7e | ||
|
|
12c7140536 | ||
|
|
2a0b605e92 | ||
|
|
6978890e6a | ||
|
|
561abd6cb9 | ||
|
|
4dd6227f0b | ||
|
|
1ec314c31c | ||
|
|
a2be5a00be | ||
|
|
4e2241c115 | ||
|
|
8459bca64a | ||
|
|
24cb0565b9 | ||
|
|
9442acb028 | ||
|
|
4f7f181a42 | ||
|
|
b7dd8737a7 | ||
|
|
2207eeb727 | ||
|
|
89dad7dfe7 | ||
|
|
e5803d0cf3 | ||
|
|
c1fffe9ae6 | ||
|
|
9e6cbd3d32 | ||
|
|
2ea8742510 | ||
|
|
5cfa0254f9 | ||
|
|
8cd2544f78 | ||
|
|
c03b768364 | ||
|
|
d60481ead4 | ||
|
|
126be3827d | ||
|
|
121274dca2 | ||
|
|
0ecf8da27e | ||
|
|
4a6bcb525d | ||
|
|
83f9ee50dd | ||
|
|
2bff297f79 | ||
|
|
dee68f6933 | ||
|
|
afa1e19c83 | ||
|
|
6052088eb4 | ||
|
|
c7fa5167c4 | ||
|
|
1034b0b146 | ||
|
|
8bcc4e5945 | ||
|
|
c3c24aa1db | ||
|
|
281c75d2d2 | ||
|
|
52307420f3 | ||
|
|
6185347cd8 | ||
|
|
b6cd29f77e | ||
|
|
b8ea8b1567 | ||
|
|
2f7dc98830 | ||
|
|
e248a99f79 | ||
|
|
4fb6d9aa5d | ||
|
|
f092ea8d67 | ||
|
|
c32cbbdda6 | ||
|
|
2497675259 | ||
|
|
8d084ab90a | ||
|
|
2398773ef0 | ||
|
|
a05998a30e | ||
|
|
f863c29194 | ||
|
|
d16a98c788 | ||
|
|
9421b02e96 | ||
|
|
10256864e4 | ||
|
|
85d010615d | ||
|
|
cd1cb186be | ||
|
|
4458354d70 | ||
|
|
0f27da8808 | ||
|
|
dd76bfa3c2 | ||
|
|
5780a66f7d | ||
|
|
d4342c034c | ||
|
|
1ec43f2530 | ||
|
|
3c300d8fdf | ||
|
|
23119b55d1 | ||
|
|
c8fb0e8f8a | ||
|
|
0ec32a77ef | ||
|
|
52921bfce8 | ||
|
|
960b929097 | ||
|
|
d4ce23eced | ||
|
|
6925510f44 | ||
|
|
9827ad4c22 | ||
|
|
ef8aaee028 | ||
|
|
3d7d39f248 | ||
|
|
3eac620560 | ||
|
|
ab17006956 | ||
|
|
bfc6889ee9 | ||
|
|
0ec0b4a044 | ||
|
|
f1a523f327 | ||
|
|
4181449aea | ||
|
|
e192f8db52 | ||
|
|
8097c681ac | ||
|
|
f45938bdd5 | ||
|
|
6ea4e97eca | ||
|
|
f274c8e837 | ||
|
|
335e571485 | ||
|
|
a11616aace | ||
|
|
883acadbc4 | ||
|
|
f51e6a3fcf | ||
|
|
371e081c0d | ||
|
|
6f41b3bf1c | ||
|
|
c1d74a6c9e | ||
|
|
24eaa6796e | ||
|
|
1521e3b620 | ||
|
|
b6ff38dd62 | ||
|
|
44ea9ac03c | ||
|
|
4c2701505b | ||
|
|
9022fe18da | ||
|
|
63be349f8b | ||
|
|
c40256a290 | ||
|
|
33ecb8ec52 | ||
|
|
82d62a0015 | ||
|
|
6278240526 | ||
|
|
8c2dc5f57d | ||
|
|
2e5868778a | ||
|
|
a10b8dab9b | ||
|
|
92f4f7ef59 | ||
|
|
31257bd5cb | ||
|
|
bb6510862f | ||
|
|
797ecf0780 | ||
|
|
f9536dc67f | ||
|
|
e8b95362af | ||
|
|
bdc39ad4ec | ||
|
|
4a202c5585 | ||
|
|
3c6b321f73 | ||
|
|
cb29b52799 | ||
|
|
7e48015a54 | ||
|
|
9ed3abf932 | ||
|
|
61762828a3 | ||
|
|
59beabe5ac | ||
|
|
0b30faa28c | ||
|
|
d12d49b93f | ||
|
|
f1d64d275a | ||
|
|
d094eeeb03 | ||
|
|
be25af658e | ||
|
|
794f52c229 | ||
|
|
5d4dc4ed4c | ||
|
|
e49d97b898 | ||
|
|
b6b4f1ba62 | ||
|
|
653d476716 | ||
|
|
48b855258c | ||
|
|
c7efdaf5f9 | ||
|
|
22523ed3d3 | ||
|
|
33c602dd61 | ||
|
|
e2a5509b76 | ||
|
|
61a0fa1a89 | ||
|
|
a35bd8292b | ||
|
|
06c8ae60e3 | ||
|
|
deeab1f845 | ||
|
|
da81c4c987 | ||
|
|
d180f1b2d5 | ||
|
|
526135629c | ||
|
|
6b9493e057 | ||
|
|
9bb33d2afc | ||
|
|
7421138533 | ||
|
|
d0800c52bb | ||
|
|
913fcd4df2 | ||
|
|
83322cc725 | ||
|
|
5944501feb | ||
|
|
17e3603d3d | ||
|
|
95be43ae47 | ||
|
|
feb91cbbaa | ||
|
|
79409af168 | ||
|
|
5dbfb64822 | ||
|
|
5e7ebf5e69 | ||
|
|
e73215ca74 | ||
|
|
a5f123b9ce | ||
|
|
ac058e9675 | ||
|
|
371b764d1d | ||
|
|
66d7172e09 | ||
|
|
99d3a8a749 | ||
|
|
db5ff372a4 | ||
|
|
3fe83f81be | ||
|
|
669e638fd6 | ||
|
|
f1f999f3b6 | ||
|
|
6f3b6fa9ce | ||
|
|
938f945301 | ||
|
|
e3efb2aad6 | ||
|
|
1e678c0d78 | ||
|
|
a59c111140 | ||
|
|
a8b2a31bed | ||
|
|
37402f9ee8 | ||
|
|
e7b5ecb40f | ||
|
|
c817ef04b9 | ||
|
|
f52b18439c | ||
|
|
1e03c628d5 | ||
|
|
71fb39db1f | ||
|
|
bcfb3726b0 | ||
|
|
c6e9e29671 | ||
|
|
1bfefcce39 | ||
|
|
22488e93e1 | ||
|
|
244b89f035 | ||
|
|
1f9a241b94 | ||
|
|
03641aae42 | ||
|
|
a2bdd113cc | ||
|
|
a92e2f3c7b | ||
|
|
97766b3a57 | ||
|
|
9ef4c3bb06 | ||
|
|
d82f0cd757 | ||
|
|
5f529e2af4 | ||
|
|
beadd9e02b | ||
|
|
72543789cb | ||
|
|
5789439fa9 | ||
|
|
f549126bcf | ||
|
|
7197548bad | ||
|
|
241fde783c | ||
|
|
2b872cd1f4 | ||
|
|
a606fb4d1d | ||
|
|
9f9c6be38e | ||
|
|
01ee524049 | ||
|
|
af9cb65338 | ||
|
|
8aa11c580b | ||
|
|
ada627f444 | ||
|
|
a7b6d338c3 | ||
|
|
9f00538b97 | ||
|
|
a085015282 | ||
|
|
0b9c220fbb | ||
|
|
0e3d04873d | ||
|
|
b7578d939f | ||
|
|
b5c28de03f | ||
|
|
e17d25c156 | ||
|
|
c25dc1b99c | ||
|
|
a493a574bd | ||
|
|
4284493dce | ||
|
|
25059de8e1 | ||
|
|
1731b05ad0 | ||
|
|
e80dc663ac | ||
|
|
39988a4c2f | ||
|
|
415bff303a | ||
|
|
a65eb62a54 | ||
|
|
03b2982128 | ||
|
|
bff0527857 | ||
|
|
f3b7634254 | ||
|
|
6a9593c0b9 | ||
|
|
edb785b8e5 | ||
|
|
26d757b50a | ||
|
|
535079ee87 | ||
|
|
ac380c29c1 | ||
|
|
3fd212f26c | ||
|
|
04a3abc651 | ||
|
|
6caf85ddd1 | ||
|
|
16e4071508 | ||
|
|
69e7c4324b | ||
|
|
a1c4a8cbe5 | ||
|
|
e37f6cfda7 | ||
|
|
989c804409 | ||
|
|
7345bc3c82 | ||
|
|
69bee35700 | ||
|
|
598e24df7c | ||
|
|
0ae669201e | ||
|
|
f52a8a4642 | ||
|
|
9c40b61ef2 | ||
|
|
72dabcda83 | ||
|
|
161a06dbcc | ||
|
|
8ed3d4e70c | ||
|
|
a4223ccc8a | ||
|
|
ca85923855 | ||
|
|
52bfe7c493 | ||
|
|
4786bd0cbe | ||
|
|
cadab160ff | ||
|
|
6a7f17b2b0 | ||
|
|
4986a4d775 | ||
|
|
903af0c2cf | ||
|
|
3282fa803c | ||
|
|
67cc47608d | ||
|
|
0411704b8b | ||
|
|
1de85b2c69 | ||
|
|
33b012f29d | ||
|
|
1357584df3 | ||
|
|
e15809e271 | ||
|
|
0da1950427 | ||
|
|
e590b921be | ||
|
|
09462692f5 | ||
|
|
c1d1b5f762 | ||
|
|
6b9c87b858 | ||
|
|
485b6eb904 | ||
|
|
057630bdb5 | ||
|
|
6b02873b30 | ||
|
|
0fa0fc6d6b | ||
|
|
339ec07465 | ||
|
|
cd2e798fea | ||
|
|
d5cadbeae2 | ||
|
|
8046a3ccae | ||
|
|
bf91d60b31 | ||
|
|
539c047ec8 | ||
|
|
290c18fa87 | ||
|
|
98c46f5e57 | ||
|
|
f8bd5b5b4e | ||
|
|
816d32edad | ||
|
|
8453835c05 | ||
|
|
9328c356c8 | ||
|
|
89e3c1fc94 | ||
|
|
67e54cd15d | ||
|
|
278ea24786 | ||
|
|
aba1662631 | ||
|
|
61eeb60c19 | ||
|
|
5e9a8f4806 | ||
|
|
4cb274e9bc | ||
|
|
8b9b1a6a35 | ||
|
|
2655964113 | ||
|
|
188bad061b | ||
|
|
3af4c329aa | ||
|
|
6c13395f7d | ||
|
|
77b32ba360 | ||
|
|
91dba291ac | ||
|
|
a6bc293640 | ||
|
|
53882d6e5f | ||
|
|
d68adfbf10 | ||
|
|
498a392d7f | ||
|
|
740f6c05db | ||
|
|
d810ce301f | ||
|
|
5ef6a14d24 | ||
|
|
a13f6f1e68 | ||
|
|
d2d0f1aaee | ||
|
|
e64c72cc89 | ||
|
|
9ab915a08b | ||
|
|
e26fbf0328 | ||
|
|
d9a52c4a2a | ||
|
|
7b2ec90de9 | ||
|
|
d310bf8bbf | ||
|
|
2abc6cc939 | ||
|
|
56d4e694a2 | ||
|
|
5f002c9cdc | ||
|
|
759daf4b4a | ||
|
|
3a8d9568e3 | ||
|
|
ff22a9d94a | ||
|
|
a6e42d5374 | ||
|
|
a2f74e0488 | ||
|
|
ee44240569 | ||
|
|
d0828744a2 | ||
|
|
6e2e576b29 | ||
|
|
bf61e27f8a | ||
|
|
c441c30b46 | ||
|
|
0e741230ea | ||
|
|
1bfe9ac2db | ||
|
|
6812e72348 | ||
|
|
b6449d2f5b | ||
|
|
7e3ea20dce | ||
|
|
c9d6fe9dcd | ||
|
|
4a649a6b8b | ||
|
|
8fef184963 | ||
|
|
69583ca3c0 | ||
|
|
6038a68e91 | ||
|
|
fa8bd8db87 | ||
|
|
18b4f0ed0f | ||
|
|
461f9d66c9 | ||
|
|
2155103c7a | ||
|
|
c9a6839c45 | ||
|
|
9fbe331a80 | ||
|
|
a56389c4ce | ||
|
|
64656784cb | ||
|
|
6eff2c181e | ||
|
|
1aa48c6d62 | ||
|
|
c7ca1a346d | ||
|
|
fa0ec7b502 | ||
|
|
768438c136 | ||
|
|
9badea0b3c | ||
|
|
43263a1650 | ||
|
|
821e02dc75 | ||
|
|
ed011ecf28 | ||
|
|
d861de4c2f | ||
|
|
3a3b2449dc | ||
|
|
d2614406ca | ||
|
|
0798d098ae | ||
|
|
dab7ddc2bb | ||
|
|
081a96e281 | ||
|
|
a7dd881d79 | ||
|
|
8134d5e24d | ||
|
|
ba6756cd45 | ||
|
|
5d8fce21ac | ||
|
|
e7e4a5bcd4 | ||
|
|
55f33357ea | ||
|
|
90568bba31 | ||
|
|
5d6e2dc2e4 | ||
|
|
6bb33f2559 | ||
|
|
ced92554ed | ||
|
|
dff3383158 | ||
|
|
bf03c89cb2 | ||
|
|
9f1484bbef | ||
|
|
3899680e26 | ||
|
|
6bb2eb25a1 | ||
|
|
f8dfd8edb3 | ||
|
|
042be624a3 | ||
|
|
6bafa4c79a | ||
|
|
58b42fac5c | ||
|
|
3b47b9558a | ||
|
|
ccf9636296 | ||
|
|
96942719f2 | ||
|
|
69cf1c1adc | ||
|
|
d77cba40b8 | ||
|
|
968735b555 | ||
|
|
ceed9d29eb | ||
|
|
41329039ee | ||
|
|
f68b102ca8 | ||
|
|
fa36e54298 | ||
|
|
b689f57435 | ||
|
|
885fa0ff56 | ||
|
|
303acb72a3 | ||
|
|
b2a46cd0cd | ||
|
|
5a5ecb3ee3 | ||
|
|
60b4ab6a63 | ||
|
|
e4b096a08f | ||
|
|
343f55049b | ||
|
|
6b46025261 | ||
|
|
5ea503f23e | ||
|
|
ce95f9ac23 | ||
|
|
c3fb87501b | ||
|
|
dc6a343612 | ||
|
|
3a61053957 | ||
|
|
570129e4d4 | ||
|
|
3315c7045f | ||
|
|
5ae50e242c | ||
|
|
bbcf449719 | ||
|
|
aab10f7184 | ||
|
|
8d43488cb8 | ||
|
|
0a9c647e19 | ||
|
|
40db5d4aa8 | ||
|
|
9254532baa | ||
|
|
7abed47cf0 | ||
|
|
5c6ac758f7 | ||
|
|
007677962c | ||
|
|
9c4aeab64a | ||
|
|
48e6fc0efe | ||
|
|
c8be713d11 | ||
|
|
ae887c8648 | ||
|
|
5daac2531b | ||
|
|
68def00327 | ||
|
|
67e7976710 | ||
|
|
35747e937e | ||
|
|
fb439787a4 | ||
|
|
8fa368f473 | ||
|
|
c84a9d07b1 | ||
|
|
7fb46cdfc4 | ||
|
|
52985e5ddc | ||
|
|
e880935dc3 | ||
|
|
cc22b1bca5 | ||
|
|
49a5128918 | ||
|
|
fedc7dcb44 | ||
|
|
cd32b20215 | ||
|
|
15cd9832c4 | ||
|
|
f25d4e4553 | ||
|
|
12d1c82b63 | ||
|
|
aebe855078 | ||
|
|
3416a71ebd | ||
|
|
94b3fea528 | ||
|
|
ad1a9ecca1 | ||
|
|
715accfb8a | ||
|
|
a8e03c6138 | ||
|
|
f69446b648 | ||
|
|
eedfbe5846 | ||
|
|
153351cc9f | ||
|
|
1b1eec40a7 | ||
|
|
763877541a | ||
|
|
1fad7d72a2 | ||
|
|
51ea2ea879 | ||
|
|
d77a478bf0 | ||
|
|
e413c0264a | ||
|
|
f88e7f898c | ||
|
|
d07bd4a6db | ||
|
|
fb34c099d5 | ||
|
|
1d2ee56a15 | ||
|
|
86665f7f09 | ||
|
|
0d2b4af986 | ||
|
|
dc2b2eeb9f | ||
|
|
e5dbb66d53 | ||
|
|
3474b1c471 | ||
|
|
3886de5b7c | ||
|
|
2b3cec06b3 | ||
|
|
8536754d14 | ||
|
|
1f36235801 | ||
|
|
a4194b14f9 | ||
|
|
2dcc629d9d | ||
|
|
98ddadc6bc | ||
|
|
f6e47b7383 | ||
|
|
f073ddc906 | ||
|
|
3e00631925 | ||
|
|
9b7ac58562 | ||
|
|
f242ddd801 | ||
|
|
c129886fe2 | ||
|
|
f577e814cf | ||
|
|
c860a0cedd | ||
|
|
ae7e28e492 | ||
|
|
90a63234ad | ||
|
|
14bca52e8f | ||
|
|
2f3c3361cf | ||
|
|
4034134055 | ||
|
|
c04f94cb7b | ||
|
|
fd1bbc7925 |
32
.devcontainer/.env.example
Normal file
32
.devcontainer/.env.example
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=trmm
|
||||||
|
|
||||||
|
IMAGE_REPO=tacticalrmm/
|
||||||
|
VERSION=latest
|
||||||
|
|
||||||
|
# tactical credentials (Used to login to dashboard)
|
||||||
|
TRMM_USER=tactical
|
||||||
|
TRMM_PASS=tactical
|
||||||
|
|
||||||
|
# dns settings
|
||||||
|
APP_HOST=rmm.example.com
|
||||||
|
API_HOST=api.example.com
|
||||||
|
MESH_HOST=mesh.example.com
|
||||||
|
|
||||||
|
# mesh settings
|
||||||
|
MESH_USER=tactical
|
||||||
|
MESH_PASS=tactical
|
||||||
|
MONGODB_USER=mongouser
|
||||||
|
MONGODB_PASSWORD=mongopass
|
||||||
|
|
||||||
|
# database settings
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASS=postgrespass
|
||||||
|
|
||||||
|
# DEV SETTINGS
|
||||||
|
APP_PORT=80
|
||||||
|
API_PORT=80
|
||||||
|
API_PROTOCOL=https://
|
||||||
|
HTTP_PROTOCOL=https
|
||||||
|
DOCKER_NETWORK=172.21.0.0/24
|
||||||
|
DOCKER_NGINX_IP=172.21.0.20
|
||||||
|
NATS_PORTS=4222:4222
|
||||||
29
.devcontainer/api.dockerfile
Normal file
29
.devcontainer/api.dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM python:3.9.6-slim
|
||||||
|
|
||||||
|
ENV TACTICAL_DIR /opt/tactical
|
||||||
|
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||||
|
ENV WORKSPACE_DIR /workspace
|
||||||
|
ENV TACTICAL_USER tactical
|
||||||
|
ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
EXPOSE 8000 8383 8005
|
||||||
|
|
||||||
|
RUN groupadd -g 1000 tactical && \
|
||||||
|
useradd -u 1000 -g 1000 tactical
|
||||||
|
|
||||||
|
# Copy nats-api file
|
||||||
|
COPY natsapi/bin/nats-api /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/nats-api
|
||||||
|
|
||||||
|
# Copy dev python reqs
|
||||||
|
COPY .devcontainer/requirements.txt /
|
||||||
|
|
||||||
|
# Copy docker entrypoint.sh
|
||||||
|
COPY .devcontainer/entrypoint.sh /
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
|
WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm
|
||||||
19
.devcontainer/docker-compose.debug.yml
Normal file
19
.devcontainer/docker-compose.debug.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api-dev:
|
||||||
|
image: api-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"]
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
- 5678:5678
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-backend
|
||||||
247
.devcontainer/docker-compose.yml
Normal file
247
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api-dev:
|
||||||
|
container_name: trmm-api-dev
|
||||||
|
image: api-dev
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/api.dockerfile
|
||||||
|
command: ["tactical-api"]
|
||||||
|
environment:
|
||||||
|
API_PORT: ${API_PORT}
|
||||||
|
ports:
|
||||||
|
- "8000:${API_PORT}"
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-backend
|
||||||
|
|
||||||
|
app-dev:
|
||||||
|
container_name: trmm-app-dev
|
||||||
|
image: node:14-alpine
|
||||||
|
restart: always
|
||||||
|
command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
|
||||||
|
working_dir: /workspace/web
|
||||||
|
volumes:
|
||||||
|
- ..:/workspace:cached
|
||||||
|
ports:
|
||||||
|
- "8080:${APP_PORT}"
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-frontend
|
||||||
|
|
||||||
|
# nats
|
||||||
|
nats-dev:
|
||||||
|
container_name: trmm-nats-dev
|
||||||
|
image: ${IMAGE_REPO}tactical-nats:${VERSION}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
API_HOST: ${API_HOST}
|
||||||
|
API_PORT: ${API_PORT}
|
||||||
|
DEV: 1
|
||||||
|
ports:
|
||||||
|
- "${NATS_PORTS}"
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- ${API_HOST}
|
||||||
|
- tactical-nats
|
||||||
|
|
||||||
|
# meshcentral container
|
||||||
|
meshcentral-dev:
|
||||||
|
container_name: trmm-meshcentral-dev
|
||||||
|
image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MESH_HOST: ${MESH_HOST}
|
||||||
|
MESH_USER: ${MESH_USER}
|
||||||
|
MESH_PASS: ${MESH_PASS}
|
||||||
|
MONGODB_USER: ${MONGODB_USER}
|
||||||
|
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
|
||||||
|
NGINX_HOST_IP: ${DOCKER_NGINX_IP}
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-meshcentral
|
||||||
|
- ${MESH_HOST}
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- mesh-data-dev:/home/node/app/meshcentral-data
|
||||||
|
depends_on:
|
||||||
|
- mongodb-dev
|
||||||
|
|
||||||
|
# mongodb container for meshcentral
|
||||||
|
mongodb-dev:
|
||||||
|
container_name: trmm-mongodb-dev
|
||||||
|
image: mongo:4.4
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
|
||||||
|
MONGO_INITDB_DATABASE: meshcentral
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-mongodb
|
||||||
|
volumes:
|
||||||
|
- mongo-dev-data:/data/db
|
||||||
|
|
||||||
|
# postgres database for api service
|
||||||
|
postgres-dev:
|
||||||
|
container_name: trmm-postgres-dev
|
||||||
|
image: postgres:13-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: tacticalrmm
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASS}
|
||||||
|
volumes:
|
||||||
|
- postgres-data-dev:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-postgres
|
||||||
|
|
||||||
|
# redis container for celery tasks
|
||||||
|
redis-dev:
|
||||||
|
container_name: trmm-redis-dev
|
||||||
|
restart: always
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
image: redis:6.0-alpine
|
||||||
|
volumes:
|
||||||
|
- redis-data-dev:/data
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-redis
|
||||||
|
|
||||||
|
init-dev:
|
||||||
|
container_name: trmm-init-dev
|
||||||
|
image: api-dev
|
||||||
|
restart: on-failure
|
||||||
|
command: ["tactical-init-dev"]
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASS: ${POSTGRES_PASS}
|
||||||
|
APP_HOST: ${APP_HOST}
|
||||||
|
API_HOST: ${API_HOST}
|
||||||
|
MESH_HOST: ${MESH_HOST}
|
||||||
|
MESH_USER: ${MESH_USER}
|
||||||
|
TRMM_USER: ${TRMM_USER}
|
||||||
|
TRMM_PASS: ${TRMM_PASS}
|
||||||
|
HTTP_PROTOCOL: ${HTTP_PROTOCOL}
|
||||||
|
APP_PORT: ${APP_PORT}
|
||||||
|
depends_on:
|
||||||
|
- postgres-dev
|
||||||
|
- meshcentral-dev
|
||||||
|
networks:
|
||||||
|
- dev
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
|
||||||
|
# container for celery worker service
|
||||||
|
celery-dev:
|
||||||
|
container_name: trmm-celery-dev
|
||||||
|
image: api-dev
|
||||||
|
command: ["tactical-celery-dev"]
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dev
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
depends_on:
|
||||||
|
- postgres-dev
|
||||||
|
- redis-dev
|
||||||
|
|
||||||
|
# container for celery beat service
|
||||||
|
celerybeat-dev:
|
||||||
|
container_name: trmm-celerybeat-dev
|
||||||
|
image: api-dev
|
||||||
|
command: ["tactical-celerybeat-dev"]
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dev
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
depends_on:
|
||||||
|
- postgres-dev
|
||||||
|
- redis-dev
|
||||||
|
|
||||||
|
# container for websockets communication
|
||||||
|
websockets-dev:
|
||||||
|
container_name: trmm-websockets-dev
|
||||||
|
image: api-dev
|
||||||
|
command: ["tactical-websockets-dev"]
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-websockets
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
depends_on:
|
||||||
|
- postgres-dev
|
||||||
|
- redis-dev
|
||||||
|
|
||||||
|
# container for tactical reverse proxy
|
||||||
|
nginx-dev:
|
||||||
|
container_name: trmm-nginx-dev
|
||||||
|
image: ${IMAGE_REPO}tactical-nginx:${VERSION}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
APP_HOST: ${APP_HOST}
|
||||||
|
API_HOST: ${API_HOST}
|
||||||
|
MESH_HOST: ${MESH_HOST}
|
||||||
|
CERT_PUB_KEY: ${CERT_PUB_KEY}
|
||||||
|
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
|
||||||
|
APP_PORT: ${APP_PORT}
|
||||||
|
API_PORT: ${API_PORT}
|
||||||
|
API_PROTOCOL: ${API_PROTOCOL}
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
ipv4_address: ${DOCKER_NGINX_IP}
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
|
||||||
|
mkdocs-dev:
|
||||||
|
container_name: trmm-mkdocs-dev
|
||||||
|
image: api-dev
|
||||||
|
restart: always
|
||||||
|
command: ["tactical-mkdocs-dev"]
|
||||||
|
ports:
|
||||||
|
- "8005:8005"
|
||||||
|
volumes:
|
||||||
|
- ..:/workspace:cached
|
||||||
|
networks:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tactical-data-dev:
|
||||||
|
postgres-data-dev:
|
||||||
|
mongo-dev-data:
|
||||||
|
mesh-data-dev:
|
||||||
|
redis-data-dev:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: ${DOCKER_NETWORK}
|
||||||
160
.devcontainer/entrypoint.sh
Normal file
160
.devcontainer/entrypoint.sh
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
: "${TRMM_USER:=tactical}"
|
||||||
|
: "${TRMM_PASS:=tactical}"
|
||||||
|
: "${POSTGRES_HOST:=tactical-postgres}"
|
||||||
|
: "${POSTGRES_PORT:=5432}"
|
||||||
|
: "${POSTGRES_USER:=tactical}"
|
||||||
|
: "${POSTGRES_PASS:=tactical}"
|
||||||
|
: "${POSTGRES_DB:=tacticalrmm}"
|
||||||
|
: "${MESH_CONTAINER:=tactical-meshcentral}"
|
||||||
|
: "${MESH_USER:=meshcentral}"
|
||||||
|
: "${MESH_PASS:=meshcentralpass}"
|
||||||
|
: "${MESH_HOST:=tactical-meshcentral}"
|
||||||
|
: "${API_HOST:=tactical-backend}"
|
||||||
|
: "${APP_HOST:=tactical-frontend}"
|
||||||
|
: "${REDIS_HOST:=tactical-redis}"
|
||||||
|
: "${HTTP_PROTOCOL:=http}"
|
||||||
|
: "${APP_PORT:=8080}"
|
||||||
|
: "${API_PORT:=8000}"
|
||||||
|
|
||||||
|
# Add python venv to path
|
||||||
|
export PATH="${VIRTUAL_ENV}/bin:$PATH"
|
||||||
|
|
||||||
|
function check_tactical_ready {
|
||||||
|
sleep 15
|
||||||
|
until [ -f "${TACTICAL_READY_FILE}" ]; do
|
||||||
|
echo "waiting for init container to finish install or update..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function django_setup {
|
||||||
|
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
|
||||||
|
echo "waiting for postgresql container to be ready..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do
|
||||||
|
echo "waiting for meshcentral container to be ready..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "setting up django environment"
|
||||||
|
|
||||||
|
# configure django settings
|
||||||
|
MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)"
|
||||||
|
|
||||||
|
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
|
||||||
|
|
||||||
|
localvars="$(cat << EOF
|
||||||
|
SECRET_KEY = '${DJANGO_SEKRET}'
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
DOCKER_BUILD = True
|
||||||
|
|
||||||
|
CERT_FILE = '/opt/tactical/certs/fullchain.pem'
|
||||||
|
KEY_FILE = '/opt/tactical/certs/privkey.pem'
|
||||||
|
|
||||||
|
SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts'
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['${API_HOST}', '*']
|
||||||
|
|
||||||
|
ADMIN_URL = 'admin/'
|
||||||
|
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': '${POSTGRES_DB}',
|
||||||
|
'USER': '${POSTGRES_USER}',
|
||||||
|
'PASSWORD': '${POSTGRES_PASS}',
|
||||||
|
'HOST': '${POSTGRES_HOST}',
|
||||||
|
'PORT': '${POSTGRES_PORT}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MESH_USERNAME = '${MESH_USER}'
|
||||||
|
MESH_SITE = 'https://${MESH_HOST}'
|
||||||
|
MESH_TOKEN_KEY = '${MESH_TOKEN}'
|
||||||
|
REDIS_HOST = '${REDIS_HOST}'
|
||||||
|
ADMIN_ENABLED = True
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
|
||||||
|
|
||||||
|
# run migrations and init scripts
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
|
||||||
|
|
||||||
|
# create super user
|
||||||
|
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-init-dev' ]; then
|
||||||
|
|
||||||
|
# make directories if they don't exist
|
||||||
|
mkdir -p "${TACTICAL_DIR}/tmp"
|
||||||
|
|
||||||
|
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
|
||||||
|
|
||||||
|
# setup Python virtual env and install dependencies
|
||||||
|
! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
|
||||||
|
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
|
||||||
|
|
||||||
|
django_setup
|
||||||
|
|
||||||
|
# create .env file for frontend
|
||||||
|
webenv="$(cat << EOF
|
||||||
|
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||||
|
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||||
|
APP_URL = "https://${APP_HOST}"
|
||||||
|
DOCKER_BUILD = 1
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
|
||||||
|
|
||||||
|
# chown everything to tactical user
|
||||||
|
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
|
||||||
|
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"
|
||||||
|
|
||||||
|
# create install ready file
|
||||||
|
su -c "echo 'tactical-init' > ${TACTICAL_READY_FILE}" "${TACTICAL_USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-api' ]; then
|
||||||
|
check_tactical_ready
|
||||||
|
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-celery-dev' ]; then
|
||||||
|
check_tactical_ready
|
||||||
|
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-celerybeat-dev' ]; then
|
||||||
|
check_tactical_ready
|
||||||
|
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
|
||||||
|
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-websockets-dev' ]; then
|
||||||
|
check_tactical_ready
|
||||||
|
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-mkdocs-dev' ]; then
|
||||||
|
cd "${WORKSPACE_DIR}/docs"
|
||||||
|
"${VIRTUAL_ENV}"/bin/mkdocs serve
|
||||||
|
fi
|
||||||
37
.devcontainer/requirements.txt
Normal file
37
.devcontainer/requirements.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
||||||
|
asyncio-nats-client
|
||||||
|
celery
|
||||||
|
channels
|
||||||
|
channels_redis
|
||||||
|
django-ipware
|
||||||
|
Django
|
||||||
|
django-cors-headers
|
||||||
|
django-rest-knox
|
||||||
|
djangorestframework
|
||||||
|
loguru
|
||||||
|
msgpack
|
||||||
|
psycopg2-binary
|
||||||
|
pycparser
|
||||||
|
pycryptodome
|
||||||
|
pyotp
|
||||||
|
pyparsing
|
||||||
|
pytz
|
||||||
|
qrcode
|
||||||
|
redis
|
||||||
|
twilio
|
||||||
|
packaging
|
||||||
|
validators
|
||||||
|
websockets
|
||||||
|
black
|
||||||
|
Werkzeug
|
||||||
|
django-extensions
|
||||||
|
coverage
|
||||||
|
coveralls
|
||||||
|
model_bakery
|
||||||
|
mkdocs
|
||||||
|
mkdocs-material
|
||||||
|
pymdown-extensions
|
||||||
|
Pygments
|
||||||
|
mypy
|
||||||
|
pysnooper
|
||||||
|
isort
|
||||||
@@ -1,5 +1,25 @@
|
|||||||
.git
|
**/__pycache__
|
||||||
.cache
|
**/.classpath
|
||||||
**/*.env
|
**/.dockerignore
|
||||||
**/env
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
**/env
|
||||||
|
README.md
|
||||||
|
|||||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: wh1te909
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: tacticalrmm
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a bug report
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Server Info (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu 20.04, Debian 10]
|
||||||
|
- Browser: [e.g. chrome, safari]
|
||||||
|
- RMM Version (as shown in top left of web UI):
|
||||||
|
|
||||||
|
**Installation Method:**
|
||||||
|
- [ ] Standard
|
||||||
|
- [ ] Docker
|
||||||
|
|
||||||
|
**Agent Info (please complete the following information):**
|
||||||
|
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
|
||||||
|
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
22
.github/workflows/deploy-docs.yml
vendored
Normal file
22
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Deploy Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: docs
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
- run: pip install --upgrade pip
|
||||||
|
- run: pip install --upgrade setuptools wheel
|
||||||
|
- run: pip install mkdocs mkdocs-material pymdown-extensions
|
||||||
|
- run: mkdocs gh-deploy --force
|
||||||
20
.github/workflows/docker-build-push.yml
vendored
20
.github/workflows/docker-build-push.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: ./docker/containers/tactical/dockerfile
|
file: ./docker/containers/tactical/dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},sadnub/tactical:latest
|
tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},tacticalrmm/tactical:latest
|
||||||
|
|
||||||
- name: Build and Push Tactical MeshCentral Image
|
- name: Build and Push Tactical MeshCentral Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: ./docker/containers/tactical-meshcentral/dockerfile
|
file: ./docker/containers/tactical-meshcentral/dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},sadnub/tactical-meshcentral:latest
|
tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},tacticalrmm/tactical-meshcentral:latest
|
||||||
|
|
||||||
- name: Build and Push Tactical NATS Image
|
- name: Build and Push Tactical NATS Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
@@ -55,17 +55,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: ./docker/containers/tactical-nats/dockerfile
|
file: ./docker/containers/tactical-nats/dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},sadnub/tactical-nats:latest
|
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
|
||||||
|
|
||||||
- name: Build and Push Tactical Salt Image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
pull: true
|
|
||||||
file: ./docker/containers/tactical-salt/dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},sadnub/tactical-salt:latest
|
|
||||||
|
|
||||||
- name: Build and Push Tactical Frontend Image
|
- name: Build and Push Tactical Frontend Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
@@ -75,7 +65,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: ./docker/containers/tactical-frontend/dockerfile
|
file: ./docker/containers/tactical-frontend/dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},sadnub/tactical-frontend:latest
|
tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},tacticalrmm/tactical-frontend:latest
|
||||||
|
|
||||||
- name: Build and Push Tactical Nginx Image
|
- name: Build and Push Tactical Nginx Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
@@ -85,4 +75,4 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: ./docker/containers/tactical-nginx/dockerfile
|
file: ./docker/containers/tactical-nginx/dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},sadnub/tactical-nginx:latest
|
tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nginx:latest
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ htmlcov/
|
|||||||
docker-compose.dev.yml
|
docker-compose.dev.yml
|
||||||
docs/.vuepress/dist
|
docs/.vuepress/dist
|
||||||
nats-rmm.conf
|
nats-rmm.conf
|
||||||
|
.mypy_cache
|
||||||
|
docs/site/
|
||||||
|
reset_db.sh
|
||||||
|
|||||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -14,6 +14,20 @@
|
|||||||
"0.0.0.0:8000"
|
"0.0.0.0:8000"
|
||||||
],
|
],
|
||||||
"django": true
|
"django": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Django: Docker Remote Attach",
|
||||||
|
"type": "python",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 5678,
|
||||||
|
"host": "localhost",
|
||||||
|
"preLaunchTask": "docker debug",
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}/api/tacticalrmm",
|
||||||
|
"remoteRoot": "/workspace/api/tacticalrmm"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
26
.vscode/settings.json
vendored
26
.vscode/settings.json
vendored
@@ -3,7 +3,14 @@
|
|||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
"python.analysis.extraPaths": [
|
"python.analysis.extraPaths": [
|
||||||
"api/tacticalrmm",
|
"api/tacticalrmm",
|
||||||
|
"api/env",
|
||||||
],
|
],
|
||||||
|
"python.analysis.diagnosticSeverityOverrides": {
|
||||||
|
"reportUnusedImport": "error",
|
||||||
|
"reportDuplicateImport": "error",
|
||||||
|
},
|
||||||
|
"python.analysis.memory.keepLibraryAst": true,
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
"python.analysis.typeCheckingMode": "basic",
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
@@ -41,4 +48,23 @@
|
|||||||
"**/*.zip": true
|
"**/*.zip": true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"[go]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": false,
|
||||||
|
},
|
||||||
|
"editor.snippetSuggestions": "none",
|
||||||
|
},
|
||||||
|
"[go.mod]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gopls": {
|
||||||
|
"usePlaceholders": true,
|
||||||
|
"completeUnimported": true,
|
||||||
|
"staticcheck": true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
23
.vscode/tasks.json
vendored
Normal file
23
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "docker debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker-compose",
|
||||||
|
"args": [
|
||||||
|
"-p",
|
||||||
|
"trmm",
|
||||||
|
"-f",
|
||||||
|
".devcontainer/docker-compose.yml",
|
||||||
|
"-f",
|
||||||
|
".devcontainer/docker-compose.debug.yml",
|
||||||
|
"up",
|
||||||
|
"-d",
|
||||||
|
"--build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
107
README.md
107
README.md
@@ -6,15 +6,15 @@
|
|||||||
[](https://github.com/python/black)
|
[](https://github.com/python/black)
|
||||||
|
|
||||||
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
|
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
|
||||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
||||||
|
|
||||||
# [LIVE DEMO](https://rmm.xlawgaming.com/)
|
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
|
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
|
||||||
|
|
||||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
|
||||||
|
|
||||||
### [Discord Chat](https://discord.gg/upGTkWp)
|
### [Discord Chat](https://discord.gg/upGTkWp)
|
||||||
|
|
||||||
|
### [Documentation](https://wh1te909.github.io/tacticalrmm/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Teamviewer-like remote desktop control
|
- Teamviewer-like remote desktop control
|
||||||
@@ -33,99 +33,6 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
|
|||||||
|
|
||||||
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
|
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
|
||||||
|
|
||||||
## Installation
|
## Installation / Backup / Restore / Usage
|
||||||
|
|
||||||
### Requirements
|
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
|
||||||
- VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04)
|
|
||||||
- A domain you own with at least 3 subdomains
|
|
||||||
- Google Authenticator app (2 factor is NOT optional)
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
Refer to the [docker setup](docker/readme.md)
|
|
||||||
|
|
||||||
|
|
||||||
### Installation example (Ubuntu server 20.04 LTS)
|
|
||||||
|
|
||||||
Fresh VPS with latest updates\
|
|
||||||
login as root and create a user and add to sudoers group (we will be creating a user called tactical)
|
|
||||||
```
|
|
||||||
apt update && apt -y upgrade
|
|
||||||
adduser tactical
|
|
||||||
usermod -a -G sudo tactical
|
|
||||||
```
|
|
||||||
|
|
||||||
switch to the tactical user and setup the firewall
|
|
||||||
```
|
|
||||||
su - tactical
|
|
||||||
sudo ufw default deny incoming
|
|
||||||
sudo ufw default allow outgoing
|
|
||||||
sudo ufw allow ssh
|
|
||||||
sudo ufw allow http
|
|
||||||
sudo ufw allow https
|
|
||||||
sudo ufw allow proto tcp from any to any port 4505,4506
|
|
||||||
sudo ufw allow proto tcp from any to any port 4222
|
|
||||||
sudo ufw enable && sudo ufw reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Our domain for this example is tacticalrmm.com
|
|
||||||
|
|
||||||
In the DNS manager of wherever our domain is hosted, we will create three A records, all pointing to the public IP address of our VPS
|
|
||||||
|
|
||||||
Create A record ```api.tacticalrmm.com``` for the django rest backend\
|
|
||||||
Create A record ```rmm.tacticalrmm.com``` for the vue frontend\
|
|
||||||
Create A record ```mesh.tacticalrmm.com``` for meshcentral
|
|
||||||
|
|
||||||
Download the install script and run it
|
|
||||||
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh
|
|
||||||
chmod +x install.sh
|
|
||||||
./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Links will be provided at the end of the install script.\
|
|
||||||
Download the executable from the first link, then open ```rmm.tacticalrmm.com``` and login.\
|
|
||||||
Upload the executable when prompted during the initial setup page.
|
|
||||||
|
|
||||||
|
|
||||||
### Install an agent
|
|
||||||
From the app's dashboard, choose Agents > Install Agent to generate an installer.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh)
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
|
|
||||||
chmod +x update.sh
|
|
||||||
./update.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup
|
|
||||||
Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh)
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
|
|
||||||
```
|
|
||||||
Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section)
|
|
||||||
|
|
||||||
Run it
|
|
||||||
```
|
|
||||||
chmod +x backup.sh
|
|
||||||
./backup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restore
|
|
||||||
Change your 3 A records to point to new server's public IP
|
|
||||||
|
|
||||||
Create same linux user account as old server and add to sudoers group and setup firewall (see install instructions above)
|
|
||||||
|
|
||||||
Copy backup file to new server
|
|
||||||
|
|
||||||
Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps.
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the restore script, passing it the backup tar file as the first argument
|
|
||||||
```
|
|
||||||
chmod +x restore.sh
|
|
||||||
./restore.sh rmm-backup-xxxxxxx.tar
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,457 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
import psutil
|
|
||||||
import os
|
|
||||||
import datetime
|
|
||||||
import zlib
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
import wmi
|
|
||||||
import win32evtlog
|
|
||||||
import win32con
|
|
||||||
import win32evtlogutil
|
|
||||||
import winerror
|
|
||||||
from time import sleep
|
|
||||||
import requests
|
|
||||||
import subprocess
|
|
||||||
import random
|
|
||||||
import platform
|
|
||||||
|
|
||||||
ARCH = "64" if platform.machine().endswith("64") else "32"
|
|
||||||
PROGRAM_DIR = os.path.join(os.environ["ProgramFiles"], "TacticalAgent")
|
|
||||||
TAC_RMM = os.path.join(PROGRAM_DIR, "tacticalrmm.exe")
|
|
||||||
NSSM = os.path.join(PROGRAM_DIR, "nssm.exe" if ARCH == "64" else "nssm-x86.exe")
|
|
||||||
TEMP_DIR = os.path.join(os.environ["WINDIR"], "Temp")
|
|
||||||
SYS_DRIVE = os.environ["SystemDrive"]
|
|
||||||
PY_BIN = os.path.join(SYS_DRIVE, "\\salt", "bin", "python.exe")
|
|
||||||
SALT_CALL = os.path.join(SYS_DRIVE, "\\salt", "salt-call.bat")
|
|
||||||
|
|
||||||
|
|
||||||
def get_services():
|
|
||||||
# see https://github.com/wh1te909/tacticalrmm/issues/38
|
|
||||||
# for why I am manually implementing the svc.as_dict() method of psutil
|
|
||||||
ret = []
|
|
||||||
for svc in psutil.win_service_iter():
|
|
||||||
i = {}
|
|
||||||
try:
|
|
||||||
i["display_name"] = svc.display_name()
|
|
||||||
i["binpath"] = svc.binpath()
|
|
||||||
i["username"] = svc.username()
|
|
||||||
i["start_type"] = svc.start_type()
|
|
||||||
i["status"] = svc.status()
|
|
||||||
i["pid"] = svc.pid()
|
|
||||||
i["name"] = svc.name()
|
|
||||||
i["description"] = svc.description()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
ret.append(i)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def run_python_script(filename, timeout, script_type="userdefined"):
|
|
||||||
# no longer used in agent version 0.11.0
|
|
||||||
file_path = os.path.join(TEMP_DIR, filename)
|
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if script_type == "userdefined":
|
|
||||||
__salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path)
|
|
||||||
else:
|
|
||||||
__salt__["cp.get_file"](f"salt://scripts/{filename}", file_path)
|
|
||||||
|
|
||||||
return __salt__["cmd.run_all"](f"{PY_BIN} {file_path}", timeout=timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def run_script(filepath, filename, shell, timeout, args=[], bg=False):
|
|
||||||
if shell == "powershell" or shell == "cmd":
|
|
||||||
if args:
|
|
||||||
return __salt__["cmd.script"](
|
|
||||||
source=filepath,
|
|
||||||
args=" ".join(map(lambda x: f'"{x}"', args)),
|
|
||||||
shell=shell,
|
|
||||||
timeout=timeout,
|
|
||||||
bg=bg,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return __salt__["cmd.script"](
|
|
||||||
source=filepath, shell=shell, timeout=timeout, bg=bg
|
|
||||||
)
|
|
||||||
|
|
||||||
elif shell == "python":
|
|
||||||
file_path = os.path.join(TEMP_DIR, filename)
|
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
__salt__["cp.get_file"](filepath, file_path)
|
|
||||||
|
|
||||||
salt_cmd = "cmd.run_bg" if bg else "cmd.run_all"
|
|
||||||
|
|
||||||
if args:
|
|
||||||
a = " ".join(map(lambda x: f'"{x}"', args))
|
|
||||||
cmd = f"{PY_BIN} {file_path} {a}"
|
|
||||||
return __salt__[salt_cmd](cmd, timeout=timeout)
|
|
||||||
else:
|
|
||||||
return __salt__[salt_cmd](f"{PY_BIN} {file_path}", timeout=timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def uninstall_agent():
|
|
||||||
remove_exe = os.path.join(PROGRAM_DIR, "unins000.exe")
|
|
||||||
__salt__["cmd.run_bg"]([remove_exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"])
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
def update_salt():
|
|
||||||
for p in psutil.process_iter():
|
|
||||||
with p.oneshot():
|
|
||||||
if p.name() == "tacticalrmm.exe" and "updatesalt" in p.cmdline():
|
|
||||||
return "running"
|
|
||||||
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
|
|
||||||
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
||||||
DETACHED_PROCESS = 0x00000008
|
|
||||||
cmd = [TAC_RMM, "-m", "updatesalt"]
|
|
||||||
p = Popen(
|
|
||||||
cmd,
|
|
||||||
stdin=PIPE,
|
|
||||||
stdout=PIPE,
|
|
||||||
stderr=PIPE,
|
|
||||||
close_fds=True,
|
|
||||||
creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
|
|
||||||
)
|
|
||||||
return p.pid
|
|
||||||
|
|
||||||
|
|
||||||
def run_manual_checks():
|
|
||||||
__salt__["cmd.run_bg"]([TAC_RMM, "-m", "runchecks"])
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
def install_updates():
|
|
||||||
for p in psutil.process_iter():
|
|
||||||
with p.oneshot():
|
|
||||||
if p.name() == "tacticalrmm.exe" and "winupdater" in p.cmdline():
|
|
||||||
return "running"
|
|
||||||
|
|
||||||
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "winupdater"])
|
|
||||||
|
|
||||||
|
|
||||||
def _wait_for_service(svc, status, retries=10):
|
|
||||||
attempts = 0
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
service = psutil.win_service_get(svc)
|
|
||||||
except psutil.NoSuchProcess:
|
|
||||||
stat = "fail"
|
|
||||||
attempts += 1
|
|
||||||
sleep(5)
|
|
||||||
else:
|
|
||||||
stat = service.status()
|
|
||||||
if stat != status:
|
|
||||||
attempts += 1
|
|
||||||
sleep(5)
|
|
||||||
else:
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
if attempts == 0 or attempts > retries:
|
|
||||||
break
|
|
||||||
|
|
||||||
return stat
|
|
||||||
|
|
||||||
|
|
||||||
def agent_update_v2(inno, url):
|
|
||||||
# make sure another instance of the update is not running
|
|
||||||
# this function spawns 2 instances of itself (because we call it twice with salt run_bg)
|
|
||||||
# so if more than 2 running, don't continue as an update is already running
|
|
||||||
count = 0
|
|
||||||
for p in psutil.process_iter():
|
|
||||||
try:
|
|
||||||
with p.oneshot():
|
|
||||||
if "win_agent.agent_update_v2" in p.cmdline():
|
|
||||||
count += 1
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if count > 2:
|
|
||||||
return "already running"
|
|
||||||
|
|
||||||
sleep(random.randint(1, 20)) # don't flood the rmm
|
|
||||||
|
|
||||||
exe = os.path.join(TEMP_DIR, inno)
|
|
||||||
|
|
||||||
if os.path.exists(exe):
|
|
||||||
try:
|
|
||||||
os.remove(exe)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = requests.get(url, stream=True, timeout=600)
|
|
||||||
except Exception:
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
if r.status_code != 200:
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
with open(exe, "wb") as f:
|
|
||||||
for chunk in r.iter_content(chunk_size=1024):
|
|
||||||
if chunk:
|
|
||||||
f.write(chunk)
|
|
||||||
del r
|
|
||||||
|
|
||||||
ret = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=120)
|
|
||||||
|
|
||||||
tac = _wait_for_service(svc="tacticalagent", status="running")
|
|
||||||
if tac != "running":
|
|
||||||
subprocess.run([NSSM, "start", "tacticalagent"], timeout=30)
|
|
||||||
|
|
||||||
chk = _wait_for_service(svc="checkrunner", status="running")
|
|
||||||
if chk != "running":
|
|
||||||
subprocess.run([NSSM, "start", "checkrunner"], timeout=30)
|
|
||||||
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
def do_agent_update_v2(inno, url):
|
|
||||||
return __salt__["cmd.run_bg"](
|
|
||||||
[
|
|
||||||
SALT_CALL,
|
|
||||||
"win_agent.agent_update_v2",
|
|
||||||
f"inno={inno}",
|
|
||||||
f"url={url}",
|
|
||||||
"--local",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def agent_update(version, url):
|
|
||||||
# make sure another instance of the update is not running
|
|
||||||
# this function spawns 2 instances of itself so if more than 2 running,
|
|
||||||
# don't continue as an update is already running
|
|
||||||
count = 0
|
|
||||||
for p in psutil.process_iter():
|
|
||||||
try:
|
|
||||||
with p.oneshot():
|
|
||||||
if "win_agent.agent_update" in p.cmdline():
|
|
||||||
count += 1
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if count > 2:
|
|
||||||
return "already running"
|
|
||||||
|
|
||||||
sleep(random.randint(1, 60)) # don't flood the rmm
|
|
||||||
try:
|
|
||||||
r = requests.get(url, stream=True, timeout=600)
|
|
||||||
except Exception:
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
if r.status_code != 200:
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
exe = os.path.join(TEMP_DIR, f"winagent-v{version}.exe")
|
|
||||||
|
|
||||||
with open(exe, "wb") as f:
|
|
||||||
for chunk in r.iter_content(chunk_size=1024):
|
|
||||||
if chunk:
|
|
||||||
f.write(chunk)
|
|
||||||
del r
|
|
||||||
|
|
||||||
services = ("tacticalagent", "checkrunner")
|
|
||||||
|
|
||||||
for svc in services:
|
|
||||||
subprocess.run([NSSM, "stop", svc], timeout=120)
|
|
||||||
|
|
||||||
sleep(10)
|
|
||||||
r = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=300)
|
|
||||||
sleep(30)
|
|
||||||
|
|
||||||
for svc in services:
|
|
||||||
subprocess.run([NSSM, "start", svc], timeout=120)
|
|
||||||
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
def do_agent_update(version, url):
|
|
||||||
return __salt__["cmd.run_bg"](
|
|
||||||
[
|
|
||||||
SALT_CALL,
|
|
||||||
"win_agent.agent_update",
|
|
||||||
f"version={version}",
|
|
||||||
f"url={url}",
|
|
||||||
"--local",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SystemDetail:
|
|
||||||
def __init__(self):
|
|
||||||
self.c = wmi.WMI()
|
|
||||||
self.comp_sys_prod = self.c.Win32_ComputerSystemProduct()
|
|
||||||
self.comp_sys = self.c.Win32_ComputerSystem()
|
|
||||||
self.memory = self.c.Win32_PhysicalMemory()
|
|
||||||
self.os = self.c.Win32_OperatingSystem()
|
|
||||||
self.base_board = self.c.Win32_BaseBoard()
|
|
||||||
self.bios = self.c.Win32_BIOS()
|
|
||||||
self.disk = self.c.Win32_DiskDrive()
|
|
||||||
self.network_adapter = self.c.Win32_NetworkAdapter()
|
|
||||||
self.network_config = self.c.Win32_NetworkAdapterConfiguration()
|
|
||||||
self.desktop_monitor = self.c.Win32_DesktopMonitor()
|
|
||||||
self.cpu = self.c.Win32_Processor()
|
|
||||||
self.usb = self.c.Win32_USBController()
|
|
||||||
|
|
||||||
def get_all(self, obj):
|
|
||||||
ret = []
|
|
||||||
for i in obj:
|
|
||||||
tmp = [
|
|
||||||
{j: getattr(i, j)}
|
|
||||||
for j in list(i.properties)
|
|
||||||
if getattr(i, j) is not None
|
|
||||||
]
|
|
||||||
ret.append(tmp)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def system_info():
|
|
||||||
info = SystemDetail()
|
|
||||||
return {
|
|
||||||
"comp_sys_prod": info.get_all(info.comp_sys_prod),
|
|
||||||
"comp_sys": info.get_all(info.comp_sys),
|
|
||||||
"mem": info.get_all(info.memory),
|
|
||||||
"os": info.get_all(info.os),
|
|
||||||
"base_board": info.get_all(info.base_board),
|
|
||||||
"bios": info.get_all(info.bios),
|
|
||||||
"disk": info.get_all(info.disk),
|
|
||||||
"network_adapter": info.get_all(info.network_adapter),
|
|
||||||
"network_config": info.get_all(info.network_config),
|
|
||||||
"desktop_monitor": info.get_all(info.desktop_monitor),
|
|
||||||
"cpu": info.get_all(info.cpu),
|
|
||||||
"usb": info.get_all(info.usb),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def local_sys_info():
|
|
||||||
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "sysinfo"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_procs():
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
# setup
|
|
||||||
for proc in psutil.process_iter():
|
|
||||||
with proc.oneshot():
|
|
||||||
proc.cpu_percent(interval=None)
|
|
||||||
|
|
||||||
# need time for psutil to record cpu percent
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
for c, proc in enumerate(psutil.process_iter(), 1):
|
|
||||||
x = {}
|
|
||||||
with proc.oneshot():
|
|
||||||
if proc.pid == 0 or not proc.name():
|
|
||||||
continue
|
|
||||||
|
|
||||||
x["name"] = proc.name()
|
|
||||||
x["cpu_percent"] = proc.cpu_percent(interval=None) / psutil.cpu_count()
|
|
||||||
x["memory_percent"] = proc.memory_percent()
|
|
||||||
x["pid"] = proc.pid
|
|
||||||
x["ppid"] = proc.ppid()
|
|
||||||
x["status"] = proc.status()
|
|
||||||
x["username"] = proc.username()
|
|
||||||
x["id"] = c
|
|
||||||
|
|
||||||
ret.append(x)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _compress_json(j):
|
|
||||||
return {
|
|
||||||
"wineventlog": base64.b64encode(
|
|
||||||
zlib.compress(json.dumps(j).encode("utf-8", errors="ignore"))
|
|
||||||
).decode("ascii", errors="ignore")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_eventlog(logtype, last_n_days):
|
|
||||||
|
|
||||||
start_time = datetime.datetime.now() - datetime.timedelta(days=last_n_days)
|
|
||||||
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
|
|
||||||
|
|
||||||
status_dict = {
|
|
||||||
win32con.EVENTLOG_AUDIT_FAILURE: "AUDIT_FAILURE",
|
|
||||||
win32con.EVENTLOG_AUDIT_SUCCESS: "AUDIT_SUCCESS",
|
|
||||||
win32con.EVENTLOG_INFORMATION_TYPE: "INFO",
|
|
||||||
win32con.EVENTLOG_WARNING_TYPE: "WARNING",
|
|
||||||
win32con.EVENTLOG_ERROR_TYPE: "ERROR",
|
|
||||||
0: "INFO",
|
|
||||||
}
|
|
||||||
|
|
||||||
computer = "localhost"
|
|
||||||
hand = win32evtlog.OpenEventLog(computer, logtype)
|
|
||||||
total = win32evtlog.GetNumberOfEventLogRecords(hand)
|
|
||||||
log = []
|
|
||||||
uid = 0
|
|
||||||
done = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
while 1:
|
|
||||||
events = win32evtlog.ReadEventLog(hand, flags, 0)
|
|
||||||
for ev_obj in events:
|
|
||||||
|
|
||||||
uid += 1
|
|
||||||
# return once total number of events reach or we'll be stuck in an infinite loop
|
|
||||||
if uid >= total:
|
|
||||||
done = True
|
|
||||||
break
|
|
||||||
|
|
||||||
the_time = ev_obj.TimeGenerated.Format()
|
|
||||||
time_obj = datetime.datetime.strptime(the_time, "%c")
|
|
||||||
if time_obj < start_time:
|
|
||||||
done = True
|
|
||||||
break
|
|
||||||
|
|
||||||
computer = str(ev_obj.ComputerName)
|
|
||||||
src = str(ev_obj.SourceName)
|
|
||||||
evt_type = str(status_dict[ev_obj.EventType])
|
|
||||||
evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID))
|
|
||||||
evt_category = str(ev_obj.EventCategory)
|
|
||||||
record = str(ev_obj.RecordNumber)
|
|
||||||
msg = (
|
|
||||||
str(win32evtlogutil.SafeFormatMessage(ev_obj, logtype))
|
|
||||||
.replace("<", "")
|
|
||||||
.replace(">", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
event_dict = {
|
|
||||||
"computer": computer,
|
|
||||||
"source": src,
|
|
||||||
"eventType": evt_type,
|
|
||||||
"eventID": evt_id,
|
|
||||||
"eventCategory": evt_category,
|
|
||||||
"message": msg,
|
|
||||||
"time": the_time,
|
|
||||||
"record": record,
|
|
||||||
"uid": uid,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.append(event_dict)
|
|
||||||
|
|
||||||
if done:
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
win32evtlog.CloseEventLog(hand)
|
|
||||||
return _compress_json(log)
|
|
||||||
@@ -20,6 +20,5 @@ omit =
|
|||||||
*/urls.py
|
*/urls.py
|
||||||
*/tests.py
|
*/tests.py
|
||||||
*/test.py
|
*/test.py
|
||||||
api/*.py
|
|
||||||
checks/utils.py
|
checks/utils.py
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from rest_framework.authtoken.admin import TokenAdmin
|
from rest_framework.authtoken.admin import TokenAdmin
|
||||||
|
|
||||||
from .models import User
|
from .models import User, Role
|
||||||
|
|
||||||
admin.site.register(User)
|
admin.site.register(User)
|
||||||
TokenAdmin.raw_id_fields = ("user",)
|
TokenAdmin.raw_id_fields = ("user",)
|
||||||
|
admin.site.register(Role)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Creates the installer user"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
if User.objects.filter(is_installer_user=True).exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
User.objects.create_user( # type: ignore
|
||||||
|
username=uuid.uuid4().hex,
|
||||||
|
is_installer_user=True,
|
||||||
|
password=User.objects.make_random_password(60), # type: ignore
|
||||||
|
)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from django.utils import timezone as djangotime
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import pyotp
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
import pyotp
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from accounts.models import User
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Generates barcode for Google Authenticator and creates totp for user"
|
help = "Generates barcode for Authenticator and creates totp for user"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument("code", type=str)
|
parser.add_argument("code", type=str)
|
||||||
@@ -24,12 +26,10 @@ class Command(BaseCommand):
|
|||||||
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
||||||
subprocess.run(f'qr "{url}"', shell=True)
|
subprocess.run(f'qr "{url}"', shell=True)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS("Scan the barcode above with your authenticator app")
|
||||||
"Scan the barcode above with your google authenticator app"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
f"If that doesn't work you may manually enter the key: {code}"
|
f"If that doesn't work you may manually enter the setup key: {code}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
57
api/tacticalrmm/accounts/management/commands/reset_2fa.py
Normal file
57
api/tacticalrmm/accounts/management/commands/reset_2fa.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Reset 2fa"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("username", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
username = kwargs["username"]
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
domain = "Tactical RMM"
|
||||||
|
nginx = "/etc/nginx/sites-available/frontend.conf"
|
||||||
|
found = None
|
||||||
|
if os.path.exists(nginx):
|
||||||
|
try:
|
||||||
|
with open(nginx, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "server_name" in line:
|
||||||
|
found = line
|
||||||
|
break
|
||||||
|
|
||||||
|
if found:
|
||||||
|
rep = found.replace("server_name", "").replace(";", "")
|
||||||
|
domain = "".join(rep.split())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
code = pyotp.random_base32()
|
||||||
|
user.totp_key = code
|
||||||
|
user.save(update_fields=["totp_key"])
|
||||||
|
|
||||||
|
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
||||||
|
subprocess.run(f'qr "{url}"', shell=True)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING("Scan the barcode above with your authenticator app")
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f"If that doesn't work you may manually enter the setup key: {code}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"2fa was successfully reset for user {username}")
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Reset password for user"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("username", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
username = kwargs["username"]
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
passwd = input("Enter new password: ")
|
||||||
|
user.set_password(passwd)
|
||||||
|
user.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
from django.db import migrations, models
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0002_auto_20200810_0544'),
|
("accounts", "0002_auto_20200810_0544"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_by',
|
name="created_by",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_time',
|
name="created_time",
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_by',
|
name="modified_by",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_time',
|
name="modified_time",
|
||||||
field=models.DateTimeField(auto_now=True, null=True),
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,24 +6,24 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0003_auto_20200922_1344'),
|
("accounts", "0003_auto_20200922_1344"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_by',
|
name="created_by",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_time',
|
name="created_time",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_by',
|
name="modified_by",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_time',
|
name="modified_time",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0004_auto_20201002_1257'),
|
("accounts", "0004_auto_20201002_1257"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_by',
|
name="created_by",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_time',
|
name="created_time",
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_by',
|
name="modified_by",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='modified_time',
|
name="modified_time",
|
||||||
field=models.DateTimeField(auto_now=True, null=True),
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.1.2 on 2020-11-10 20:24
|
# Generated by Django 3.1.2 on 2020-11-10 20:24
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0007_update_agent_primary_key'),
|
("accounts", "0007_update_agent_primary_key"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='dark_mode',
|
name="dark_mode",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2020-12-10 17:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0008_user_dark_mode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="show_community_scripts",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-01-14 01:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0009_user_show_community_scripts"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="agent_dblclick_action",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("editagent", "Edit Agent"),
|
||||||
|
("takecontrol", "Take Control"),
|
||||||
|
("remotebg", "Remote Background"),
|
||||||
|
],
|
||||||
|
default="editagent",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.1.5 on 2021-01-18 09:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0010_user_agent_dblclick_action"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="default_agent_tbl_tab",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("server", "Servers"),
|
||||||
|
("workstation", "Workstations"),
|
||||||
|
("mixed", "Mixed"),
|
||||||
|
],
|
||||||
|
default="server",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-02-28 06:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_user_default_agent_tbl_tab'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='agents_per_page',
|
||||||
|
field=models.PositiveIntegerField(default=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-09 02:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0012_user_agents_per_page'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='client_tree_sort',
|
||||||
|
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-04-11 01:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0013_user_client_tree_sort'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='client_tree_splitter',
|
||||||
|
field=models.PositiveIntegerField(default=11),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-04-11 03:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0014_user_client_tree_splitter'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='loading_bar_color',
|
||||||
|
field=models.CharField(default='red', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-05-07 15:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0022_urlaction'),
|
||||||
|
('accounts', '0015_user_loading_bar_color'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='url_action',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='agent_dblclick_action',
|
||||||
|
field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-05-08 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0016_auto_20210507_1526'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_code_sign',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_do_server_maint',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_edit_agent',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_edit_core_settings',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_install_agents',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_accounts',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_alerts',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_automation_policies',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_autotasks',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_checks',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_clients',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_deployments',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_notes',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_pendingactions',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_procs',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_scripts',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_sites',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_software',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_winsvcs',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_winupdates',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_reboot_agents',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_autotasks',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_bulk',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_checks',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_scripts',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_send_cmd',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_uninstall_agents',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_update_agents',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_use_mesh',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_auditlogs',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_debuglogs',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_eventlogs',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0017_auto_20210508_1716'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Role',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True)),
|
||||||
|
('is_superuser', models.BooleanField(default=False)),
|
||||||
|
('can_use_mesh', models.BooleanField(default=False)),
|
||||||
|
('can_uninstall_agents', models.BooleanField(default=False)),
|
||||||
|
('can_update_agents', models.BooleanField(default=False)),
|
||||||
|
('can_edit_agent', models.BooleanField(default=False)),
|
||||||
|
('can_manage_procs', models.BooleanField(default=False)),
|
||||||
|
('can_view_eventlogs', models.BooleanField(default=False)),
|
||||||
|
('can_send_cmd', models.BooleanField(default=False)),
|
||||||
|
('can_reboot_agents', models.BooleanField(default=False)),
|
||||||
|
('can_install_agents', models.BooleanField(default=False)),
|
||||||
|
('can_run_scripts', models.BooleanField(default=False)),
|
||||||
|
('can_run_bulk', models.BooleanField(default=False)),
|
||||||
|
('can_manage_notes', models.BooleanField(default=False)),
|
||||||
|
('can_edit_core_settings', models.BooleanField(default=False)),
|
||||||
|
('can_do_server_maint', models.BooleanField(default=False)),
|
||||||
|
('can_code_sign', models.BooleanField(default=False)),
|
||||||
|
('can_manage_checks', models.BooleanField(default=False)),
|
||||||
|
('can_run_checks', models.BooleanField(default=False)),
|
||||||
|
('can_manage_clients', models.BooleanField(default=False)),
|
||||||
|
('can_manage_sites', models.BooleanField(default=False)),
|
||||||
|
('can_manage_deployments', models.BooleanField(default=False)),
|
||||||
|
('can_manage_automation_policies', models.BooleanField(default=False)),
|
||||||
|
('can_manage_autotasks', models.BooleanField(default=False)),
|
||||||
|
('can_run_autotasks', models.BooleanField(default=False)),
|
||||||
|
('can_view_auditlogs', models.BooleanField(default=False)),
|
||||||
|
('can_manage_pendingactions', models.BooleanField(default=False)),
|
||||||
|
('can_view_debuglogs', models.BooleanField(default=False)),
|
||||||
|
('can_manage_scripts', models.BooleanField(default=False)),
|
||||||
|
('can_manage_alerts', models.BooleanField(default=False)),
|
||||||
|
('can_manage_winsvcs', models.BooleanField(default=False)),
|
||||||
|
('can_manage_software', models.BooleanField(default=False)),
|
||||||
|
('can_manage_winupdates', models.BooleanField(default=False)),
|
||||||
|
('can_manage_accounts', models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_code_sign',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_do_server_maint',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_edit_agent',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_edit_core_settings',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_install_agents',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_accounts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_alerts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_automation_policies',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_autotasks',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_checks',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_clients',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_deployments',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_notes',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_pendingactions',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_procs',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_scripts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_sites',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_software',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_winsvcs',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_manage_winupdates',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_reboot_agents',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_autotasks',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_bulk',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_checks',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_run_scripts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_send_cmd',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_uninstall_agents',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_update_agents',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_use_mesh',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_auditlogs',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_debuglogs',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='can_view_eventlogs',
|
||||||
|
),
|
||||||
|
]
|
||||||
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0018_auto_20210511_0233"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="role",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="roles",
|
||||||
|
to="accounts.role",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-05-11 17:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0019_user_role'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='can_manage_roles',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-06-17 04:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0020_role_can_manage_roles'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='can_view_core_settings',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-06-28 05:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0021_role_can_view_core_settings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='clear_search_when_switching',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-06-30 03:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0022_user_clear_search_when_switching'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='is_installer_user',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-20 20:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0023_user_is_installer_user'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='last_login_ip',
|
||||||
|
field=models.GenericIPAddressField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-21 04:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0024_user_last_login_ip'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='created_by',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='created_time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='modified_time',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-09-01 12:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0025_auto_20210721_0424'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIKey',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_by', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('created_time', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('modified_by', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('modified_time', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('name', models.CharField(max_length=25, unique=True)),
|
||||||
|
('key', models.CharField(blank=True, max_length=48, unique=True)),
|
||||||
|
('expiration', models.DateTimeField(blank=True, default=None, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='can_manage_api_keys',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-09-03 00:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0026_auto_20210901_1247'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apikey',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='block_dashboard_login',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,13 +1,56 @@
|
|||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields import CharField, DateTimeField
|
||||||
|
|
||||||
from logs.models import BaseAuditModel
|
from logs.models import BaseAuditModel
|
||||||
|
|
||||||
|
AGENT_DBLCLICK_CHOICES = [
|
||||||
|
("editagent", "Edit Agent"),
|
||||||
|
("takecontrol", "Take Control"),
|
||||||
|
("remotebg", "Remote Background"),
|
||||||
|
("urlaction", "URL Action"),
|
||||||
|
]
|
||||||
|
|
||||||
|
AGENT_TBL_TAB_CHOICES = [
|
||||||
|
("server", "Servers"),
|
||||||
|
("workstation", "Workstations"),
|
||||||
|
("mixed", "Mixed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
CLIENT_TREE_SORT_CHOICES = [
|
||||||
|
("alphafail", "Move failing clients to the top"),
|
||||||
|
("alpha", "Sort alphabetically"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser, BaseAuditModel):
|
class User(AbstractUser, BaseAuditModel):
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
block_dashboard_login = models.BooleanField(default=False)
|
||||||
totp_key = models.CharField(max_length=50, null=True, blank=True)
|
totp_key = models.CharField(max_length=50, null=True, blank=True)
|
||||||
dark_mode = models.BooleanField(default=True)
|
dark_mode = models.BooleanField(default=True)
|
||||||
|
show_community_scripts = models.BooleanField(default=True)
|
||||||
|
agent_dblclick_action = models.CharField(
|
||||||
|
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
|
||||||
|
)
|
||||||
|
url_action = models.ForeignKey(
|
||||||
|
"core.URLAction",
|
||||||
|
related_name="user",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
default_agent_tbl_tab = models.CharField(
|
||||||
|
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||||
|
)
|
||||||
|
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
|
||||||
|
client_tree_sort = models.CharField(
|
||||||
|
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
|
||||||
|
)
|
||||||
|
client_tree_splitter = models.PositiveIntegerField(default=11)
|
||||||
|
loading_bar_color = models.CharField(max_length=255, default="red")
|
||||||
|
clear_search_when_switching = models.BooleanField(default=True)
|
||||||
|
is_installer_user = models.BooleanField(default=False)
|
||||||
|
last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True)
|
||||||
|
|
||||||
agent = models.OneToOneField(
|
agent = models.OneToOneField(
|
||||||
"agents.Agent",
|
"agents.Agent",
|
||||||
@@ -17,9 +60,153 @@ class User(AbstractUser, BaseAuditModel):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
role = models.ForeignKey(
|
||||||
|
"accounts.Role",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="roles",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serialize(user):
|
def serialize(user):
|
||||||
# serializes the task and returns json
|
# serializes the task and returns json
|
||||||
from .serializers import UserSerializer
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
return UserSerializer(user).data
|
return UserSerializer(user).data
|
||||||
|
|
||||||
|
|
||||||
|
class Role(BaseAuditModel):
|
||||||
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
is_superuser = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# agents
|
||||||
|
can_use_mesh = models.BooleanField(default=False)
|
||||||
|
can_uninstall_agents = models.BooleanField(default=False)
|
||||||
|
can_update_agents = models.BooleanField(default=False)
|
||||||
|
can_edit_agent = models.BooleanField(default=False)
|
||||||
|
can_manage_procs = models.BooleanField(default=False)
|
||||||
|
can_view_eventlogs = models.BooleanField(default=False)
|
||||||
|
can_send_cmd = models.BooleanField(default=False)
|
||||||
|
can_reboot_agents = models.BooleanField(default=False)
|
||||||
|
can_install_agents = models.BooleanField(default=False)
|
||||||
|
can_run_scripts = models.BooleanField(default=False)
|
||||||
|
can_run_bulk = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# core
|
||||||
|
can_manage_notes = models.BooleanField(default=False)
|
||||||
|
can_view_core_settings = models.BooleanField(default=False)
|
||||||
|
can_edit_core_settings = models.BooleanField(default=False)
|
||||||
|
can_do_server_maint = models.BooleanField(default=False)
|
||||||
|
can_code_sign = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# checks
|
||||||
|
can_manage_checks = models.BooleanField(default=False)
|
||||||
|
can_run_checks = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# clients
|
||||||
|
can_manage_clients = models.BooleanField(default=False)
|
||||||
|
can_manage_sites = models.BooleanField(default=False)
|
||||||
|
can_manage_deployments = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# automation
|
||||||
|
can_manage_automation_policies = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# automated tasks
|
||||||
|
can_manage_autotasks = models.BooleanField(default=False)
|
||||||
|
can_run_autotasks = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# logs
|
||||||
|
can_view_auditlogs = models.BooleanField(default=False)
|
||||||
|
can_manage_pendingactions = models.BooleanField(default=False)
|
||||||
|
can_view_debuglogs = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# scripts
|
||||||
|
can_manage_scripts = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# alerts
|
||||||
|
can_manage_alerts = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# win services
|
||||||
|
can_manage_winsvcs = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# software
|
||||||
|
can_manage_software = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# windows updates
|
||||||
|
can_manage_winupdates = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# accounts
|
||||||
|
can_manage_accounts = models.BooleanField(default=False)
|
||||||
|
can_manage_roles = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# authentication
|
||||||
|
can_manage_api_keys = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize(role):
|
||||||
|
# serializes the agent and returns json
|
||||||
|
from .serializers import RoleAuditSerializer
|
||||||
|
|
||||||
|
return RoleAuditSerializer(role).data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perms():
|
||||||
|
return [
|
||||||
|
"is_superuser",
|
||||||
|
"can_use_mesh",
|
||||||
|
"can_uninstall_agents",
|
||||||
|
"can_update_agents",
|
||||||
|
"can_edit_agent",
|
||||||
|
"can_manage_procs",
|
||||||
|
"can_view_eventlogs",
|
||||||
|
"can_send_cmd",
|
||||||
|
"can_reboot_agents",
|
||||||
|
"can_install_agents",
|
||||||
|
"can_run_scripts",
|
||||||
|
"can_run_bulk",
|
||||||
|
"can_manage_notes",
|
||||||
|
"can_view_core_settings",
|
||||||
|
"can_edit_core_settings",
|
||||||
|
"can_do_server_maint",
|
||||||
|
"can_code_sign",
|
||||||
|
"can_manage_checks",
|
||||||
|
"can_run_checks",
|
||||||
|
"can_manage_clients",
|
||||||
|
"can_manage_sites",
|
||||||
|
"can_manage_deployments",
|
||||||
|
"can_manage_automation_policies",
|
||||||
|
"can_manage_autotasks",
|
||||||
|
"can_run_autotasks",
|
||||||
|
"can_view_auditlogs",
|
||||||
|
"can_manage_pendingactions",
|
||||||
|
"can_view_debuglogs",
|
||||||
|
"can_manage_scripts",
|
||||||
|
"can_manage_alerts",
|
||||||
|
"can_manage_winsvcs",
|
||||||
|
"can_manage_software",
|
||||||
|
"can_manage_winupdates",
|
||||||
|
"can_manage_accounts",
|
||||||
|
"can_manage_roles",
|
||||||
|
"can_manage_api_keys",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class APIKey(BaseAuditModel):
|
||||||
|
name = CharField(unique=True, max_length=25)
|
||||||
|
key = CharField(unique=True, blank=True, max_length=48)
|
||||||
|
expiration = DateTimeField(blank=True, null=True, default=None)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"accounts.User",
|
||||||
|
related_name="api_key",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize(apikey):
|
||||||
|
from .serializers import APIKeyAuditSerializer
|
||||||
|
|
||||||
|
return APIKeyAuditSerializer(apikey).data
|
||||||
|
|||||||
40
api/tacticalrmm/accounts/permissions.py
Normal file
40
api/tacticalrmm/accounts/permissions.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from tacticalrmm.permissions import _has_perm
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
if r.method == "GET":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# allow users to reset their own password/2fa see issue #686
|
||||||
|
base_path = "/accounts/users/"
|
||||||
|
paths = ["reset/", "reset_totp/"]
|
||||||
|
|
||||||
|
if r.path in [base_path + i for i in paths]:
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(pk=r.data["id"])
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if user == r.user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return _has_perm(r, "can_manage_accounts")
|
||||||
|
|
||||||
|
|
||||||
|
class RolesPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
if r.method == "GET":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return _has_perm(r, "can_manage_roles")
|
||||||
|
|
||||||
|
|
||||||
|
class APIKeyPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
|
||||||
|
return _has_perm(r, "can_manage_api_keys")
|
||||||
@@ -1,17 +1,34 @@
|
|||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
|
ReadOnlyField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import User
|
from .models import APIKey, User, Role
|
||||||
|
|
||||||
|
|
||||||
|
class UserUISerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"dark_mode",
|
||||||
|
"show_community_scripts",
|
||||||
|
"agent_dblclick_action",
|
||||||
|
"url_action",
|
||||||
|
"default_agent_tbl_tab",
|
||||||
|
"client_tree_sort",
|
||||||
|
"client_tree_splitter",
|
||||||
|
"loading_bar_color",
|
||||||
|
"clear_search_when_switching",
|
||||||
|
"block_dashboard_login",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = (
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"username",
|
"username",
|
||||||
"first_name",
|
"first_name",
|
||||||
@@ -19,7 +36,10 @@ class UserSerializer(ModelSerializer):
|
|||||||
"email",
|
"email",
|
||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
)
|
"last_login_ip",
|
||||||
|
"role",
|
||||||
|
"block_dashboard_login",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TOTPSetupSerializer(ModelSerializer):
|
class TOTPSetupSerializer(ModelSerializer):
|
||||||
@@ -38,3 +58,36 @@ class TOTPSetupSerializer(ModelSerializer):
|
|||||||
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
|
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
|
||||||
obj.username, issuer_name="Tactical RMM"
|
obj.username, issuer_name="Tactical RMM"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAuditSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class APIKeySerializer(ModelSerializer):
|
||||||
|
|
||||||
|
username = ReadOnlyField(source="user.username")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = APIKey
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class APIKeyAuditSerializer(ModelSerializer):
|
||||||
|
username = ReadOnlyField(source="user.username")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = APIKey
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"expiration",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import override_settings
|
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
from model_bakery import baker, seq
|
||||||
|
from accounts.models import User, APIKey
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
from accounts.models import User
|
|
||||||
|
from accounts.serializers import APIKeySerializer
|
||||||
|
|
||||||
|
|
||||||
class TestAccounts(TacticalTestCase):
|
class TestAccounts(TacticalTestCase):
|
||||||
@@ -38,6 +41,12 @@ class TestAccounts(TacticalTestCase):
|
|||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, "ok")
|
self.assertEqual(r.data, "ok")
|
||||||
|
|
||||||
|
# test user set to block dashboard logins
|
||||||
|
self.bob.block_dashboard_login = True
|
||||||
|
self.bob.save()
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
@patch("pyotp.TOTP.verify")
|
@patch("pyotp.TOTP.verify")
|
||||||
def test_login_view(self, mock_verify):
|
def test_login_view(self, mock_verify):
|
||||||
url = "/login/"
|
url = "/login/"
|
||||||
@@ -155,6 +164,33 @@ class GetUpdateDeleteUser(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_put_root_user(self):
|
||||||
|
url = f"/accounts/{self.john.pk}/users/"
|
||||||
|
data = {
|
||||||
|
"id": self.john.pk,
|
||||||
|
"username": "john",
|
||||||
|
"email": "johndoe@xlawgaming.com",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
}
|
||||||
|
r = self.client.put(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_put_not_root_user(self):
|
||||||
|
url = f"/accounts/{self.john.pk}/users/"
|
||||||
|
data = {
|
||||||
|
"id": self.john.pk,
|
||||||
|
"username": "john",
|
||||||
|
"email": "johndoe@xlawgaming.com",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
}
|
||||||
|
self.client.force_authenticate(user=self.alice)
|
||||||
|
r = self.client.put(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
url = f"/accounts/{self.john.pk}/users/"
|
url = f"/accounts/{self.john.pk}/users/"
|
||||||
r = self.client.delete(url)
|
r = self.client.delete(url)
|
||||||
@@ -166,6 +202,19 @@ class GetUpdateDeleteUser(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("delete", url)
|
self.check_not_authenticated("delete", url)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_delete_root_user(self):
|
||||||
|
url = f"/accounts/{self.john.pk}/users/"
|
||||||
|
r = self.client.delete(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_delete_non_root_user(self):
|
||||||
|
url = f"/accounts/{self.john.pk}/users/"
|
||||||
|
self.client.force_authenticate(user=self.alice)
|
||||||
|
r = self.client.delete(url)
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
|
|
||||||
class TestUserAction(TacticalTestCase):
|
class TestUserAction(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -184,6 +233,21 @@ class TestUserAction(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_post_root_user(self):
|
||||||
|
url = "/accounts/users/reset/"
|
||||||
|
data = {"id": self.john.pk, "password": "3ASDjh2345kJA!@#)#@__123"}
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_post_non_root_user(self):
|
||||||
|
url = "/accounts/users/reset/"
|
||||||
|
data = {"id": self.john.pk, "password": "3ASDjh2345kJA!@#)#@__123"}
|
||||||
|
self.client.force_authenticate(user=self.alice)
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
def test_put(self):
|
def test_put(self):
|
||||||
url = "/accounts/users/reset/"
|
url = "/accounts/users/reset/"
|
||||||
data = {"id": self.john.pk}
|
data = {"id": self.john.pk}
|
||||||
@@ -195,15 +259,105 @@ class TestUserAction(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
def test_darkmode(self):
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_put_root_user(self):
|
||||||
|
url = "/accounts/users/reset/"
|
||||||
|
data = {"id": self.john.pk}
|
||||||
|
r = self.client.put(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
user = User.objects.get(pk=self.john.pk)
|
||||||
|
self.assertEqual(user.totp_key, "")
|
||||||
|
|
||||||
|
@override_settings(ROOT_USER="john")
|
||||||
|
def test_put_non_root_user(self):
|
||||||
|
url = "/accounts/users/reset/"
|
||||||
|
data = {"id": self.john.pk}
|
||||||
|
self.client.force_authenticate(user=self.alice)
|
||||||
|
r = self.client.put(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
|
def test_user_ui(self):
|
||||||
url = "/accounts/users/ui/"
|
url = "/accounts/users/ui/"
|
||||||
data = {"dark_mode": False}
|
|
||||||
|
data = {
|
||||||
|
"dark_mode": True,
|
||||||
|
"show_community_scripts": True,
|
||||||
|
"agent_dblclick_action": "editagent",
|
||||||
|
"default_agent_tbl_tab": "mixed",
|
||||||
|
"client_tree_sort": "alpha",
|
||||||
|
"client_tree_splitter": 14,
|
||||||
|
"loading_bar_color": "green",
|
||||||
|
"clear_search_when_switching": False,
|
||||||
|
}
|
||||||
r = self.client.patch(url, data, format="json")
|
r = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPIKeyViews(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_coresettings()
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
def test_get_api_keys(self):
|
||||||
|
url = "/accounts/apikeys/"
|
||||||
|
apikeys = baker.make("accounts.APIKey", key=seq("APIKEY"), _quantity=3)
|
||||||
|
|
||||||
|
serializer = APIKeySerializer(apikeys, many=True)
|
||||||
|
resp = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(serializer.data, resp.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
def test_add_api_keys(self):
|
||||||
|
url = "/accounts/apikeys/"
|
||||||
|
|
||||||
|
user = baker.make("accounts.User")
|
||||||
|
data = {"name": "Name", "user": user.id, "expiration": None}
|
||||||
|
|
||||||
|
resp = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(APIKey.objects.filter(name="Name").exists())
|
||||||
|
self.assertTrue(APIKey.objects.get(name="Name").key)
|
||||||
|
|
||||||
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
def test_modify_api_key(self):
|
||||||
|
# test a call where api key doesn't exist
|
||||||
|
resp = self.client.put("/accounts/apikeys/500/", format="json")
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
apikey = baker.make("accounts.APIKey", name="Test")
|
||||||
|
url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
|
||||||
|
|
||||||
|
data = {"name": "New Name"} # type: ignore
|
||||||
|
|
||||||
|
resp = self.client.put(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
apikey = APIKey.objects.get(pk=apikey.pk) # type: ignore
|
||||||
|
self.assertEquals(apikey.name, "New Name")
|
||||||
|
|
||||||
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
|
def test_delete_api_key(self):
|
||||||
|
# test a call where api key doesn't exist
|
||||||
|
resp = self.client.delete("/accounts/apikeys/500/", format="json")
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
# test delete api key
|
||||||
|
apikey = baker.make("accounts.APIKey")
|
||||||
|
url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
|
||||||
|
resp = self.client.delete(url, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("delete", url)
|
||||||
|
|
||||||
|
|
||||||
class TestTOTPSetup(TacticalTestCase):
|
class TestTOTPSetup(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
@@ -229,3 +383,29 @@ class TestTOTPSetup(TacticalTestCase):
|
|||||||
r = self.client.post(url)
|
r = self.client.post(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, "totp token already set")
|
self.assertEqual(r.data, "totp token already set")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPIAuthentication(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# create User and associate to API Key
|
||||||
|
self.user = User.objects.create(username="api_user", is_superuser=True)
|
||||||
|
self.api_key = APIKey.objects.create(
|
||||||
|
name="Test Token", key="123456", user=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client_setup()
|
||||||
|
|
||||||
|
def test_api_auth(self):
|
||||||
|
url = "/clients/clients/"
|
||||||
|
# auth should fail if no header set
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
# invalid api key in header should return code 400
|
||||||
|
self.client.credentials(HTTP_X_API_KEY="000000")
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 401)
|
||||||
|
|
||||||
|
# valid api key in header should return code 200
|
||||||
|
self.client.credentials(HTTP_X_API_KEY="123456")
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -8,4 +9,9 @@ urlpatterns = [
|
|||||||
path("users/reset_totp/", views.UserActions.as_view()),
|
path("users/reset_totp/", views.UserActions.as_view()),
|
||||||
path("users/setup_totp/", views.TOTPSetup.as_view()),
|
path("users/setup_totp/", views.TOTPSetup.as_view()),
|
||||||
path("users/ui/", views.UserUI.as_view()),
|
path("users/ui/", views.UserUI.as_view()),
|
||||||
|
path("permslist/", views.PermsList.as_view()),
|
||||||
|
path("roles/", views.GetAddRoles.as_view()),
|
||||||
|
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
|
||||||
|
path("apikeys/", views.GetAddAPIKeys.as_view()),
|
||||||
|
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404
|
from django.contrib.auth import login
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.views import APIView
|
from ipware import get_client_ip
|
||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
|
||||||
from knox.views import LoginView as KnoxLoginView
|
from knox.views import LoginView as KnoxLoginView
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from .models import User
|
|
||||||
from agents.models import Agent
|
|
||||||
from logs.models import AuditLog
|
from logs.models import AuditLog
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
from tacticalrmm.utils import notify_error
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .serializers import UserSerializer, TOTPSetupSerializer
|
from .models import APIKey, Role, User
|
||||||
|
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
|
||||||
|
from .serializers import (
|
||||||
|
APIKeySerializer,
|
||||||
|
RoleSerializer,
|
||||||
|
TOTPSetupSerializer,
|
||||||
|
UserSerializer,
|
||||||
|
UserUISerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_root_user(request, user) -> bool:
|
||||||
|
return (
|
||||||
|
hasattr(settings, "ROOT_USER")
|
||||||
|
and request.user != user
|
||||||
|
and user.username == settings.ROOT_USER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CheckCreds(KnoxLoginView):
|
class CheckCreds(KnoxLoginView):
|
||||||
@@ -29,11 +41,16 @@ class CheckCreds(KnoxLoginView):
|
|||||||
# check credentials
|
# check credentials
|
||||||
serializer = AuthTokenSerializer(data=request.data)
|
serializer = AuthTokenSerializer(data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
AuditLog.audit_user_failed_login(request.data["username"])
|
AuditLog.audit_user_failed_login(
|
||||||
|
request.data["username"], debug_info={"ip": request._client_ip}
|
||||||
|
)
|
||||||
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
user = serializer.validated_data["user"]
|
user = serializer.validated_data["user"]
|
||||||
|
|
||||||
|
if user.block_dashboard_login:
|
||||||
|
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# if totp token not set modify response to notify frontend
|
# if totp token not set modify response to notify frontend
|
||||||
if not user.totp_key:
|
if not user.totp_key:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
@@ -60,28 +77,47 @@ class LoginView(KnoxLoginView):
|
|||||||
|
|
||||||
if settings.DEBUG and token == "sekret":
|
if settings.DEBUG and token == "sekret":
|
||||||
valid = True
|
valid = True
|
||||||
elif totp.verify(token, valid_window=1):
|
elif totp.verify(token, valid_window=10):
|
||||||
valid = True
|
valid = True
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
AuditLog.audit_user_login_successful(request.data["username"])
|
|
||||||
|
# save ip information
|
||||||
|
client_ip, is_routable = get_client_ip(request)
|
||||||
|
user.last_login_ip = client_ip
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
AuditLog.audit_user_login_successful(
|
||||||
|
request.data["username"], debug_info={"ip": request._client_ip}
|
||||||
|
)
|
||||||
return super(LoginView, self).post(request, format=None)
|
return super(LoginView, self).post(request, format=None)
|
||||||
else:
|
else:
|
||||||
AuditLog.audit_user_failed_twofactor(request.data["username"])
|
AuditLog.audit_user_failed_twofactor(
|
||||||
|
request.data["username"], debug_info={"ip": request._client_ip}
|
||||||
|
)
|
||||||
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class GetAddUsers(APIView):
|
class GetAddUsers(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
users = User.objects.filter(agent=None)
|
search = request.GET.get("search", None)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
users = User.objects.filter(agent=None, is_installer_user=False).filter(
|
||||||
|
username__icontains=search
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
users = User.objects.filter(agent=None, is_installer_user=False)
|
||||||
|
|
||||||
return Response(UserSerializer(users, many=True).data)
|
return Response(UserSerializer(users, many=True).data)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# add new user
|
# add new user
|
||||||
try:
|
try:
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user( # type: ignore
|
||||||
request.data["username"],
|
request.data["username"],
|
||||||
request.data["email"],
|
request.data["email"],
|
||||||
request.data["password"],
|
request.data["password"],
|
||||||
@@ -91,15 +127,21 @@ class GetAddUsers(APIView):
|
|||||||
f"ERROR: User {request.data['username']} already exists!"
|
f"ERROR: User {request.data['username']} already exists!"
|
||||||
)
|
)
|
||||||
|
|
||||||
user.first_name = request.data["first_name"]
|
if "first_name" in request.data.keys():
|
||||||
user.last_name = request.data["last_name"]
|
user.first_name = request.data["first_name"]
|
||||||
# Can be changed once permissions and groups are introduced
|
if "last_name" in request.data.keys():
|
||||||
user.is_superuser = True
|
user.last_name = request.data["last_name"]
|
||||||
|
if "role" in request.data.keys() and isinstance(request.data["role"], int):
|
||||||
|
role = get_object_or_404(Role, pk=request.data["role"])
|
||||||
|
user.role = role
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
return Response(user.username)
|
return Response(user.username)
|
||||||
|
|
||||||
|
|
||||||
class GetUpdateDeleteUser(APIView):
|
class GetUpdateDeleteUser(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
user = get_object_or_404(User, pk=pk)
|
user = get_object_or_404(User, pk=pk)
|
||||||
|
|
||||||
@@ -108,6 +150,9 @@ class GetUpdateDeleteUser(APIView):
|
|||||||
def put(self, request, pk):
|
def put(self, request, pk):
|
||||||
user = get_object_or_404(User, pk=pk)
|
user = get_object_or_404(User, pk=pk)
|
||||||
|
|
||||||
|
if _is_root_user(request, user):
|
||||||
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
@@ -115,17 +160,23 @@ class GetUpdateDeleteUser(APIView):
|
|||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
def delete(self, request, pk):
|
def delete(self, request, pk):
|
||||||
get_object_or_404(User, pk=pk).delete()
|
user = get_object_or_404(User, pk=pk)
|
||||||
|
if _is_root_user(request, user):
|
||||||
|
return notify_error("The root user cannot be deleted from the UI")
|
||||||
|
|
||||||
|
user.delete()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
class UserActions(APIView):
|
class UserActions(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||||
# reset password
|
# reset password
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
||||||
user = get_object_or_404(User, pk=request.data["id"])
|
user = get_object_or_404(User, pk=request.data["id"])
|
||||||
|
if _is_root_user(request, user):
|
||||||
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
user.set_password(request.data["password"])
|
user.set_password(request.data["password"])
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -133,8 +184,10 @@ class UserActions(APIView):
|
|||||||
|
|
||||||
# reset two factor token
|
# reset two factor token
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
|
|
||||||
user = get_object_or_404(User, pk=request.data["id"])
|
user = get_object_or_404(User, pk=request.data["id"])
|
||||||
|
if _is_root_user(request, user):
|
||||||
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
user.totp_key = ""
|
user.totp_key = ""
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -160,7 +213,93 @@ class TOTPSetup(APIView):
|
|||||||
|
|
||||||
class UserUI(APIView):
|
class UserUI(APIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = request.user
|
serializer = UserUISerializer(
|
||||||
user.dark_mode = request.data["dark_mode"]
|
instance=request.user, data=request.data, partial=True
|
||||||
user.save(update_fields=["dark_mode"])
|
)
|
||||||
return Response("ok")
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class PermsList(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
return Response(Role.perms())
|
||||||
|
|
||||||
|
|
||||||
|
class GetAddRoles(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, RolesPerms]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
roles = Role.objects.all()
|
||||||
|
return Response(RoleSerializer(roles, many=True).data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RoleSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class GetUpdateDeleteRole(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, RolesPerms]
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
role = get_object_or_404(Role, pk=pk)
|
||||||
|
return Response(RoleSerializer(role).data)
|
||||||
|
|
||||||
|
def put(self, request, pk):
|
||||||
|
role = get_object_or_404(Role, pk=pk)
|
||||||
|
serializer = RoleSerializer(instance=role, data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
role = get_object_or_404(Role, pk=pk)
|
||||||
|
role.delete()
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class GetAddAPIKeys(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, APIKeyPerms]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
apikeys = APIKey.objects.all()
|
||||||
|
return Response(APIKeySerializer(apikeys, many=True).data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# generate a random API Key
|
||||||
|
# https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
request.data["key"] = "".join(
|
||||||
|
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||||
|
for _ in range(32)
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = APIKeySerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
obj = serializer.save()
|
||||||
|
return Response("The API Key was added")
|
||||||
|
|
||||||
|
|
||||||
|
class GetUpdateDeleteAPIKey(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, APIKeyPerms]
|
||||||
|
|
||||||
|
def put(self, request, pk):
|
||||||
|
apikey = get_object_or_404(APIKey, pk=pk)
|
||||||
|
|
||||||
|
# remove API key is present in request data
|
||||||
|
if "key" in request.data.keys():
|
||||||
|
request.data.pop("key")
|
||||||
|
|
||||||
|
serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response("The API Key was edited")
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
apikey = get_object_or_404(APIKey, pk=pk)
|
||||||
|
apikey.delete()
|
||||||
|
return Response("The API Key was deleted")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
|
||||||
|
|
||||||
admin.site.register(Agent)
|
admin.site.register(Agent)
|
||||||
admin.site.register(AgentOutage)
|
|
||||||
admin.site.register(RecoveryAction)
|
admin.site.register(RecoveryAction)
|
||||||
admin.site.register(Note)
|
admin.site.register(Note)
|
||||||
|
admin.site.register(AgentCustomField)
|
||||||
|
admin.site.register(AgentHistory)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
from model_bakery.recipe import Recipe, seq
|
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from django.utils import timezone as djangotime
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from .models import Agent
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
from model_bakery.recipe import Recipe, foreign_key, seq
|
||||||
|
|
||||||
|
|
||||||
def generate_agent_id(hostname):
|
def generate_agent_id(hostname):
|
||||||
@@ -16,6 +14,9 @@ def generate_agent_id(hostname):
|
|||||||
return f"{rand}-{hostname}"
|
return f"{rand}-{hostname}"
|
||||||
|
|
||||||
|
|
||||||
|
site = Recipe("clients.Site")
|
||||||
|
|
||||||
|
|
||||||
def get_wmi_data():
|
def get_wmi_data():
|
||||||
with open(
|
with open(
|
||||||
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
|
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
|
||||||
@@ -24,12 +25,12 @@ def get_wmi_data():
|
|||||||
|
|
||||||
|
|
||||||
agent = Recipe(
|
agent = Recipe(
|
||||||
Agent,
|
"agents.Agent",
|
||||||
|
site=foreign_key(site),
|
||||||
hostname="DESKTOP-TEST123",
|
hostname="DESKTOP-TEST123",
|
||||||
version="1.1.1",
|
version="1.3.0",
|
||||||
monitoring_type=cycle(["workstation", "server"]),
|
monitoring_type=cycle(["workstation", "server"]),
|
||||||
salt_id=generate_agent_id("DESKTOP-TEST123"),
|
agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
|
||||||
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
server_agent = agent.extend(
|
server_agent = agent.extend(
|
||||||
@@ -42,8 +43,12 @@ workstation_agent = agent.extend(
|
|||||||
|
|
||||||
online_agent = agent.extend(last_seen=djangotime.now())
|
online_agent = agent.extend(last_seen=djangotime.now())
|
||||||
|
|
||||||
|
offline_agent = agent.extend(
|
||||||
|
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
|
||||||
|
)
|
||||||
|
|
||||||
overdue_agent = agent.extend(
|
overdue_agent = agent.extend(
|
||||||
last_seen=djangotime.now() - djangotime.timedelta(minutes=6)
|
last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
|
||||||
)
|
)
|
||||||
|
|
||||||
agent_with_services = agent.extend(
|
agent_with_services = agent.extend(
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
from clients.models import Client, Site
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Bulk update agent offline/overdue time"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("time", type=int, help="Time in minutes")
|
||||||
|
parser.add_argument(
|
||||||
|
"--client",
|
||||||
|
type=str,
|
||||||
|
help="Client Name",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--site",
|
||||||
|
type=str,
|
||||||
|
help="Site Name",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--offline",
|
||||||
|
action="store_true",
|
||||||
|
help="Offline",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--overdue",
|
||||||
|
action="store_true",
|
||||||
|
help="Overdue",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="All agents",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
time = kwargs["time"]
|
||||||
|
client_name = kwargs["client"]
|
||||||
|
site_name = kwargs["site"]
|
||||||
|
all_agents = kwargs["all"]
|
||||||
|
offline = kwargs["offline"]
|
||||||
|
overdue = kwargs["overdue"]
|
||||||
|
agents = None
|
||||||
|
|
||||||
|
if offline and time < 2:
|
||||||
|
self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if overdue and time < 3:
|
||||||
|
self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if client_name:
|
||||||
|
try:
|
||||||
|
client = Client.objects.get(name=client_name)
|
||||||
|
except Client.DoesNotExist:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f"Client {client_name} doesn't exist")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
agents = Agent.objects.filter(site__client=client)
|
||||||
|
|
||||||
|
elif site_name:
|
||||||
|
try:
|
||||||
|
site = Site.objects.get(name=site_name)
|
||||||
|
except Site.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
agents = Agent.objects.filter(site=site)
|
||||||
|
|
||||||
|
elif all_agents:
|
||||||
|
agents = Agent.objects.all()
|
||||||
|
|
||||||
|
if agents:
|
||||||
|
if offline:
|
||||||
|
agents.update(offline_time=time)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Changed offline time on {len(agents)} agents to {time} minutes"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if overdue:
|
||||||
|
agents.update(overdue_time=time)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Changed overdue time on {len(agents)} agents to {time} minutes"
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Shows online agents that are not on the latest version"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
|
||||||
|
"pk", "version", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
agents = [i for i in q if i.status == "online"]
|
||||||
|
for agent in agents:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
|
||||||
|
)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 3.0.6 on 2020-05-31 01:23
|
# Generated by Django 3.0.6 on 2020-05-31 01:23
|
||||||
|
|
||||||
import django.contrib.postgres.fields.jsonb
|
import django.contrib.postgres.fields.jsonb
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.0.7 on 2020-06-09 16:07
|
# Generated by Django 3.0.7 on 2020-06-09 16:07
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.0.8 on 2020-08-09 05:31
|
# Generated by Django 3.0.8 on 2020-08-09 05:31
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 3.1.1 on 2020-09-22 20:57
|
# Generated by Django 3.1.1 on 2020-09-22 20:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
# Generated by Django 3.1.2 on 2020-11-01 22:53
|
# Generated by Django 3.1.2 on 2020-11-01 22:53
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('clients', '0006_deployment'),
|
("clients", "0006_deployment"),
|
||||||
('agents', '0020_auto_20201025_2129'),
|
("agents", "0020_auto_20201025_2129"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='agent',
|
model_name="agent",
|
||||||
name='site_link',
|
name="site_link",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='clients.site'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="agents",
|
||||||
|
to="clients.site",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('agents', '0022_update_site_primary_key'),
|
("agents", "0022_update_site_primary_key"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='agent',
|
model_name="agent",
|
||||||
name='client',
|
name="client",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='agent',
|
model_name="agent",
|
||||||
name='site',
|
name="site",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('agents', '0023_auto_20201101_2312'),
|
("agents", "0023_auto_20201101_2312"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='agent',
|
model_name="agent",
|
||||||
old_name='site_link',
|
old_name="site_link",
|
||||||
new_name='site',
|
new_name="site",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,22 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('agents', '0024_auto_20201101_2319'),
|
("agents", "0024_auto_20201101_2319"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='recoveryaction',
|
model_name="recoveryaction",
|
||||||
name='mode',
|
name="mode",
|
||||||
field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC')], default='mesh', max_length=50),
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("salt", "Salt"),
|
||||||
|
("mesh", "Mesh"),
|
||||||
|
("command", "Command"),
|
||||||
|
("rpc", "Nats RPC"),
|
||||||
|
],
|
||||||
|
default="mesh",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,23 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('agents', '0025_auto_20201122_0407'),
|
("agents", "0025_auto_20201122_0407"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='recoveryaction',
|
model_name="recoveryaction",
|
||||||
name='mode',
|
name="mode",
|
||||||
field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC'), ('checkrunner', 'Checkrunner')], default='mesh', max_length=50),
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("salt", "Salt"),
|
||||||
|
("mesh", "Mesh"),
|
||||||
|
("command", "Command"),
|
||||||
|
("rpc", "Nats RPC"),
|
||||||
|
("checkrunner", "Checkrunner"),
|
||||||
|
],
|
||||||
|
default="mesh",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-01-29 21:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0026_auto_20201125_2334'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='overdue_dashboard_alert',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
Normal file
23
api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-02-06 15:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0027_agent_overdue_dashboard_alert'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentoutage',
|
||||||
|
name='outage_email_sent_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentoutage',
|
||||||
|
name='outage_sms_sent_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
16
api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
Normal file
16
api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-02-10 21:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0028_auto_20210206_1534'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='AgentOutage',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
Normal file
18
api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-16 08:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0029_delete_agentoutage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='offline_time',
|
||||||
|
field=models.PositiveIntegerField(default=4),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-04 03:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0006_auto_20210217_1736'),
|
||||||
|
('agents', '0030_agent_offline_time'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='alert_template',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_customfield'),
|
||||||
|
('agents', '0031_agent_alert_template'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgentCustomField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('value', models.TextField(blank=True, null=True)),
|
||||||
|
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
|
||||||
|
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 02:51
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0032_agentcustomfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
name='multiple_value',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 03:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0033_agentcustomfield_multiple_value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
name='checkbox_value',
|
||||||
|
field=models.BooleanField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0034_agentcustomfield_checkbox_value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
old_name='checkbox_value',
|
||||||
|
new_name='bool_value',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
old_name='value',
|
||||||
|
new_name='string_value',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-04-17 01:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0035_auto_20210329_1709'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='block_policy_inheritance',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
Normal file
23
api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-06-27 00:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0036_agent_block_policy_inheritance'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='has_patches_pending',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='pending_actions_count',
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
api/tacticalrmm/agents/migrations/0038_agenthistory.py
Normal file
27
api/tacticalrmm/agents/migrations/0038_agenthistory.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-06 02:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0037_auto_20210627_0014'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgentHistory',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('time', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('type', models.CharField(choices=[('task_run', 'Task Run'), ('script_run', 'Script Run'), ('cmd_run', 'CMD Run')], default='cmd_run', max_length=50)),
|
||||||
|
('command', models.TextField(blank=True, null=True)),
|
||||||
|
('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure')], default='success', max_length=50)),
|
||||||
|
('username', models.CharField(default='system', max_length=50)),
|
||||||
|
('results', models.TextField(blank=True, null=True)),
|
||||||
|
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='agents.agent')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
25
api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
Normal file
25
api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-07-14 07:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('scripts', '0008_script_guid'),
|
||||||
|
('agents', '0038_agenthistory'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenthistory',
|
||||||
|
name='script',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenthistory',
|
||||||
|
name='script_results',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
63
api/tacticalrmm/agents/permissions.py
Normal file
63
api/tacticalrmm/agents/permissions.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from tacticalrmm.permissions import _has_perm
|
||||||
|
|
||||||
|
|
||||||
|
class MeshPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_use_mesh")
|
||||||
|
|
||||||
|
|
||||||
|
class UninstallPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_uninstall_agents")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAgentPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_update_agents")
|
||||||
|
|
||||||
|
|
||||||
|
class EditAgentPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_edit_agent")
|
||||||
|
|
||||||
|
|
||||||
|
class ManageProcPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_manage_procs")
|
||||||
|
|
||||||
|
|
||||||
|
class EvtLogPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_view_eventlogs")
|
||||||
|
|
||||||
|
|
||||||
|
class SendCMDPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_send_cmd")
|
||||||
|
|
||||||
|
|
||||||
|
class RebootAgentPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_reboot_agents")
|
||||||
|
|
||||||
|
|
||||||
|
class InstallAgentPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_install_agents")
|
||||||
|
|
||||||
|
|
||||||
|
class RunScriptPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_run_scripts")
|
||||||
|
|
||||||
|
|
||||||
|
class ManageNotesPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_manage_notes")
|
||||||
|
|
||||||
|
|
||||||
|
class RunBulkPerms(permissions.BasePermission):
|
||||||
|
def has_permission(self, r, view):
|
||||||
|
return _has_perm(r, "can_run_bulk")
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.fields import ReadOnlyField
|
|
||||||
|
|
||||||
from .models import Agent, Note
|
|
||||||
|
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
|
||||||
from clients.serializers import ClientSerializer
|
from clients.serializers import ClientSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from tacticalrmm.utils import get_default_timezone
|
||||||
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
|
from .models import Agent, AgentCustomField, Note, AgentHistory
|
||||||
|
|
||||||
|
|
||||||
class AgentSerializer(serializers.ModelSerializer):
|
class AgentSerializer(serializers.ModelSerializer):
|
||||||
# for vue
|
# for vue
|
||||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
|
||||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||||
status = serializers.ReadOnlyField()
|
status = serializers.ReadOnlyField()
|
||||||
cpu_model = serializers.ReadOnlyField()
|
cpu_model = serializers.ReadOnlyField()
|
||||||
local_ips = serializers.ReadOnlyField()
|
local_ips = serializers.ReadOnlyField()
|
||||||
make_model = serializers.ReadOnlyField()
|
make_model = serializers.ReadOnlyField()
|
||||||
physical_disks = serializers.ReadOnlyField()
|
physical_disks = serializers.ReadOnlyField()
|
||||||
|
graphics = serializers.ReadOnlyField()
|
||||||
checks = serializers.ReadOnlyField()
|
checks = serializers.ReadOnlyField()
|
||||||
timezone = serializers.ReadOnlyField()
|
timezone = serializers.ReadOnlyField()
|
||||||
all_timezones = serializers.SerializerMethodField()
|
all_timezones = serializers.SerializerMethodField()
|
||||||
@@ -34,30 +32,64 @@ class AgentSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AgentOverdueActionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Agent
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"overdue_email_alert",
|
||||||
|
"overdue_text_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AgentTableSerializer(serializers.ModelSerializer):
|
class AgentTableSerializer(serializers.ModelSerializer):
|
||||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
|
||||||
pending_actions = serializers.SerializerMethodField()
|
|
||||||
status = serializers.ReadOnlyField()
|
status = serializers.ReadOnlyField()
|
||||||
checks = serializers.ReadOnlyField()
|
checks = serializers.ReadOnlyField()
|
||||||
last_seen = serializers.SerializerMethodField()
|
last_seen = serializers.SerializerMethodField()
|
||||||
client_name = serializers.ReadOnlyField(source="client.name")
|
client_name = serializers.ReadOnlyField(source="client.name")
|
||||||
site_name = serializers.ReadOnlyField(source="site.name")
|
site_name = serializers.ReadOnlyField(source="site.name")
|
||||||
|
logged_username = serializers.SerializerMethodField()
|
||||||
|
italic = serializers.SerializerMethodField()
|
||||||
|
policy = serializers.ReadOnlyField(source="policy.id")
|
||||||
|
alert_template = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_pending_actions(self, obj):
|
def get_alert_template(self, obj):
|
||||||
return obj.pendingactions.filter(status="pending").count()
|
|
||||||
|
|
||||||
def get_last_seen(self, obj):
|
if not obj.alert_template:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"name": obj.alert_template.name,
|
||||||
|
"always_email": obj.alert_template.agent_always_email,
|
||||||
|
"always_text": obj.alert_template.agent_always_text,
|
||||||
|
"always_alert": obj.alert_template.agent_always_alert,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_last_seen(self, obj) -> str:
|
||||||
if obj.time_zone is not None:
|
if obj.time_zone is not None:
|
||||||
agent_tz = pytz.timezone(obj.time_zone)
|
agent_tz = pytz.timezone(obj.time_zone)
|
||||||
else:
|
else:
|
||||||
agent_tz = self.context["default_tz"]
|
agent_tz = self.context["default_tz"]
|
||||||
|
|
||||||
return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S")
|
return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M")
|
||||||
|
|
||||||
|
def get_logged_username(self, obj) -> str:
|
||||||
|
if obj.logged_in_username == "None" and obj.status == "online":
|
||||||
|
return obj.last_logged_in_user
|
||||||
|
elif obj.logged_in_username != "None":
|
||||||
|
return obj.logged_in_username
|
||||||
|
else:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def get_italic(self, obj) -> bool:
|
||||||
|
return obj.logged_in_username == "None" and obj.status == "online"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Agent
|
model = Agent
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
|
"alert_template",
|
||||||
"hostname",
|
"hostname",
|
||||||
"agent_id",
|
"agent_id",
|
||||||
"site_name",
|
"site_name",
|
||||||
@@ -65,25 +97,48 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
|||||||
"monitoring_type",
|
"monitoring_type",
|
||||||
"description",
|
"description",
|
||||||
"needs_reboot",
|
"needs_reboot",
|
||||||
"patches_pending",
|
"has_patches_pending",
|
||||||
"pending_actions",
|
"pending_actions_count",
|
||||||
"status",
|
"status",
|
||||||
"overdue_text_alert",
|
"overdue_text_alert",
|
||||||
"overdue_email_alert",
|
"overdue_email_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
"last_seen",
|
"last_seen",
|
||||||
"boot_time",
|
"boot_time",
|
||||||
"checks",
|
"checks",
|
||||||
"logged_in_username",
|
|
||||||
"last_logged_in_user",
|
|
||||||
"maintenance_mode",
|
"maintenance_mode",
|
||||||
|
"logged_username",
|
||||||
|
"italic",
|
||||||
|
"policy",
|
||||||
|
"block_policy_inheritance",
|
||||||
]
|
]
|
||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCustomFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = AgentCustomField
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"field",
|
||||||
|
"agent",
|
||||||
|
"value",
|
||||||
|
"string_value",
|
||||||
|
"bool_value",
|
||||||
|
"multiple_value",
|
||||||
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
"string_value": {"write_only": True},
|
||||||
|
"bool_value": {"write_only": True},
|
||||||
|
"multiple_value": {"write_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AgentEditSerializer(serializers.ModelSerializer):
|
class AgentEditSerializer(serializers.ModelSerializer):
|
||||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||||
all_timezones = serializers.SerializerMethodField()
|
all_timezones = serializers.SerializerMethodField()
|
||||||
client = ClientSerializer(read_only=True)
|
client = ClientSerializer(read_only=True)
|
||||||
|
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
def get_all_timezones(self, obj):
|
def get_all_timezones(self, obj):
|
||||||
return pytz.all_timezones
|
return pytz.all_timezones
|
||||||
@@ -101,19 +156,18 @@ class AgentEditSerializer(serializers.ModelSerializer):
|
|||||||
"timezone",
|
"timezone",
|
||||||
"check_interval",
|
"check_interval",
|
||||||
"overdue_time",
|
"overdue_time",
|
||||||
|
"offline_time",
|
||||||
"overdue_text_alert",
|
"overdue_text_alert",
|
||||||
"overdue_email_alert",
|
"overdue_email_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
"all_timezones",
|
"all_timezones",
|
||||||
"winupdatepolicy",
|
"winupdatepolicy",
|
||||||
|
"policy",
|
||||||
|
"custom_fields",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WinAgentSerializer(serializers.ModelSerializer):
|
class WinAgentSerializer(serializers.ModelSerializer):
|
||||||
# for the windows agent
|
|
||||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
|
||||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
|
||||||
status = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Agent
|
model = Agent
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -147,3 +201,22 @@ class NotesSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Agent
|
model = Agent
|
||||||
fields = ["hostname", "pk", "notes"]
|
fields = ["hostname", "pk", "notes"]
|
||||||
|
|
||||||
|
|
||||||
|
class AgentHistorySerializer(serializers.ModelSerializer):
|
||||||
|
time = serializers.SerializerMethodField(read_only=True)
|
||||||
|
script_name = serializers.ReadOnlyField(source="script.name")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AgentHistory
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def get_time(self, history):
|
||||||
|
timezone = get_default_timezone()
|
||||||
|
return history.time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAuditSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Agent
|
||||||
|
exclude = ["disks", "services", "wmi_detail"]
|
||||||
|
|||||||
@@ -1,293 +1,393 @@
|
|||||||
from loguru import logger
|
import asyncio
|
||||||
from time import sleep
|
import datetime as dt
|
||||||
import random
|
import random
|
||||||
import requests
|
import urllib.parse
|
||||||
from packaging import version as pyver
|
from time import sleep
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from alerts.models import Alert
|
||||||
|
from core.models import CodeSignToken, CoreSettings
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
from logs.models import DebugLog, PendingAction
|
||||||
|
from packaging import version as pyver
|
||||||
|
from scripts.models import Script
|
||||||
from tacticalrmm.celery import app
|
from tacticalrmm.celery import app
|
||||||
from agents.models import Agent, AgentOutage
|
from tacticalrmm.utils import run_nats_api_cmd
|
||||||
from core.models import CoreSettings
|
|
||||||
from logs.models import PendingAction
|
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
from agents.models import Agent
|
||||||
|
|
||||||
OLD_64_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2.exe"
|
|
||||||
OLD_32_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2-x86.exe"
|
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
|
||||||
|
from agents.utils import get_exegen_url
|
||||||
|
|
||||||
|
agent = Agent.objects.get(pk=pk)
|
||||||
|
|
||||||
|
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
|
||||||
|
return "not supported"
|
||||||
|
|
||||||
|
# skip if we can't determine the arch
|
||||||
|
if agent.arch is None:
|
||||||
|
DebugLog.warning(
|
||||||
|
agent=agent,
|
||||||
|
log_type="agent_issues",
|
||||||
|
message=f"Unable to determine arch on {agent.hostname}({agent.pk}). Skipping agent update.",
|
||||||
|
)
|
||||||
|
return "noarch"
|
||||||
|
|
||||||
|
version = settings.LATEST_AGENT_VER
|
||||||
|
inno = agent.win_inno_exe
|
||||||
|
|
||||||
|
if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
|
||||||
|
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||||
|
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
|
||||||
|
url = base_url + urllib.parse.urlencode(params)
|
||||||
|
else:
|
||||||
|
url = agent.winagent_dl
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
if agent.pendingactions.filter(
|
||||||
|
action_type="agentupdate", status="pending"
|
||||||
|
).exists():
|
||||||
|
agent.pendingactions.filter(
|
||||||
|
action_type="agentupdate", status="pending"
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
PendingAction.objects.create(
|
||||||
|
agent=agent,
|
||||||
|
action_type="agentupdate",
|
||||||
|
details={
|
||||||
|
"url": url,
|
||||||
|
"version": version,
|
||||||
|
"inno": inno,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
nats_data = {
|
||||||
|
"func": "agentupdate",
|
||||||
|
"payload": {
|
||||||
|
"url": url,
|
||||||
|
"version": version,
|
||||||
|
"inno": inno,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||||
|
return "created"
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def send_agent_update_task(pks, version):
|
def force_code_sign(pks: list[int]) -> None:
|
||||||
assert isinstance(pks, list)
|
try:
|
||||||
|
token = CodeSignToken.objects.first().token # type:ignore
|
||||||
q = Agent.objects.filter(pk__in=pks)
|
except:
|
||||||
agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)]
|
|
||||||
|
|
||||||
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
|
|
||||||
|
|
||||||
for chunk in chunks:
|
|
||||||
for pk in chunk:
|
|
||||||
agent = Agent.objects.get(pk=pk)
|
|
||||||
|
|
||||||
# skip if we can't determine the arch
|
|
||||||
if agent.arch is None:
|
|
||||||
logger.warning(
|
|
||||||
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
|
||||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
|
||||||
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
|
|
||||||
inno = (
|
|
||||||
"winagent-v0.11.2.exe"
|
|
||||||
if agent.arch == "64"
|
|
||||||
else "winagent-v0.11.2-x86.exe"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
url = agent.winagent_dl
|
|
||||||
inno = agent.win_inno_exe
|
|
||||||
|
|
||||||
if agent.has_nats:
|
|
||||||
if agent.pendingactions.filter(
|
|
||||||
action_type="agentupdate", status="pending"
|
|
||||||
).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
PendingAction.objects.create(
|
|
||||||
agent=agent,
|
|
||||||
action_type="agentupdate",
|
|
||||||
details={
|
|
||||||
"url": agent.winagent_dl,
|
|
||||||
"version": settings.LATEST_AGENT_VER,
|
|
||||||
"inno": agent.win_inno_exe,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# TODO
|
|
||||||
# Salt is deprecated, remove this once salt is gone
|
|
||||||
else:
|
|
||||||
r = agent.salt_api_async(
|
|
||||||
func="win_agent.do_agent_update_v2",
|
|
||||||
kwargs={
|
|
||||||
"inno": inno,
|
|
||||||
"url": url,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def auto_self_agent_update_task():
|
|
||||||
core = CoreSettings.objects.first()
|
|
||||||
if not core.agent_auto_update:
|
|
||||||
logger.info("Agent auto update is disabled. Skipping.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
|
||||||
|
for chunk in chunks:
|
||||||
|
for pk in chunk:
|
||||||
|
agent_update(pk=pk, codesigntoken=token, force=True)
|
||||||
|
sleep(0.05)
|
||||||
|
sleep(4)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def send_agent_update_task(pks: list[int]) -> None:
|
||||||
|
try:
|
||||||
|
codesigntoken = CodeSignToken.objects.first().token # type:ignore
|
||||||
|
except:
|
||||||
|
codesigntoken = None
|
||||||
|
|
||||||
|
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||||
|
for chunk in chunks:
|
||||||
|
for pk in chunk:
|
||||||
|
agent_update(pk, codesigntoken)
|
||||||
|
sleep(0.05)
|
||||||
|
sleep(4)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def auto_self_agent_update_task() -> None:
|
||||||
|
core = CoreSettings.objects.first()
|
||||||
|
if not core.agent_auto_update: # type:ignore
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
codesigntoken = CodeSignToken.objects.first().token # type:ignore
|
||||||
|
except:
|
||||||
|
codesigntoken = None
|
||||||
|
|
||||||
q = Agent.objects.only("pk", "version")
|
q = Agent.objects.only("pk", "version")
|
||||||
agents = [
|
pks: list[int] = [
|
||||||
i.pk
|
i.pk
|
||||||
for i in q
|
for i in q
|
||||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
]
|
]
|
||||||
|
|
||||||
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
|
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
for pk in chunk:
|
for pk in chunk:
|
||||||
agent = Agent.objects.get(pk=pk)
|
agent_update(pk, codesigntoken)
|
||||||
|
sleep(0.05)
|
||||||
# skip if we can't determine the arch
|
sleep(4)
|
||||||
if agent.arch is None:
|
|
||||||
logger.warning(
|
|
||||||
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
|
||||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
|
||||||
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
|
|
||||||
inno = (
|
|
||||||
"winagent-v0.11.2.exe"
|
|
||||||
if agent.arch == "64"
|
|
||||||
else "winagent-v0.11.2-x86.exe"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
url = agent.winagent_dl
|
|
||||||
inno = agent.win_inno_exe
|
|
||||||
|
|
||||||
if agent.has_nats:
|
|
||||||
if agent.pendingactions.filter(
|
|
||||||
action_type="agentupdate", status="pending"
|
|
||||||
).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
PendingAction.objects.create(
|
|
||||||
agent=agent,
|
|
||||||
action_type="agentupdate",
|
|
||||||
details={
|
|
||||||
"url": agent.winagent_dl,
|
|
||||||
"version": settings.LATEST_AGENT_VER,
|
|
||||||
"inno": agent.win_inno_exe,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# TODO
|
|
||||||
# Salt is deprecated, remove this once salt is gone
|
|
||||||
else:
|
|
||||||
r = agent.salt_api_async(
|
|
||||||
func="win_agent.do_agent_update_v2",
|
|
||||||
kwargs={
|
|
||||||
"inno": inno,
|
|
||||||
"url": url,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def sync_salt_modules_task(pk):
|
def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||||
agent = Agent.objects.get(pk=pk)
|
from alerts.models import Alert
|
||||||
r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules")
|
|
||||||
# successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]}
|
|
||||||
# successful sync with no new/changed files: {'return': [{'MINION-15': []}]}
|
|
||||||
if r == "timeout" or r == "error":
|
|
||||||
return f"Unable to sync modules {agent.salt_id}"
|
|
||||||
|
|
||||||
return f"Successfully synced salt modules on {agent.hostname}"
|
alert = Alert.objects.get(pk=pk)
|
||||||
|
|
||||||
|
if not alert.email_sent:
|
||||||
@app.task
|
sleep(random.randint(1, 15))
|
||||||
def batch_sync_modules_task():
|
alert.agent.send_outage_email()
|
||||||
# sync modules, split into chunks of 50 agents to not overload salt
|
alert.email_sent = djangotime.now()
|
||||||
agents = Agent.objects.all()
|
alert.save(update_fields=["email_sent"])
|
||||||
online = [i.salt_id for i in agents]
|
|
||||||
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
|
|
||||||
for chunk in chunks:
|
|
||||||
Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
|
|
||||||
sleep(10)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def uninstall_agent_task(salt_id, has_nats):
|
|
||||||
attempts = 0
|
|
||||||
error = False
|
|
||||||
|
|
||||||
if not has_nats:
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
f"http://{settings.SALT_HOST}:8123/run",
|
|
||||||
json=[
|
|
||||||
{
|
|
||||||
"client": "local",
|
|
||||||
"tgt": salt_id,
|
|
||||||
"fun": "win_agent.uninstall_agent",
|
|
||||||
"timeout": 8,
|
|
||||||
"username": settings.SALT_USERNAME,
|
|
||||||
"password": settings.SALT_PASSWORD,
|
|
||||||
"eauth": "pam",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
ret = r.json()["return"][0][salt_id]
|
|
||||||
except Exception:
|
|
||||||
attempts += 1
|
|
||||||
else:
|
|
||||||
if ret != "ok":
|
|
||||||
attempts += 1
|
|
||||||
else:
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
if attempts >= 10:
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
elif attempts == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
if error:
|
|
||||||
logger.error(f"{salt_id} uninstall failed")
|
|
||||||
else:
|
else:
|
||||||
logger.info(f"{salt_id} was successfully uninstalled")
|
if alert_interval:
|
||||||
|
# send an email only if the last email sent is older than alert interval
|
||||||
try:
|
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||||
r = requests.post(
|
if alert.email_sent < delta:
|
||||||
f"http://{settings.SALT_HOST}:8123/run",
|
sleep(random.randint(1, 10))
|
||||||
json=[
|
alert.agent.send_outage_email()
|
||||||
{
|
alert.email_sent = djangotime.now()
|
||||||
"client": "wheel",
|
alert.save(update_fields=["email_sent"])
|
||||||
"fun": "key.delete",
|
|
||||||
"match": salt_id,
|
|
||||||
"username": settings.SALT_USERNAME,
|
|
||||||
"password": settings.SALT_PASSWORD,
|
|
||||||
"eauth": "pam",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.error(f"{salt_id} unable to remove salt-key")
|
|
||||||
|
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def agent_outage_email_task(pk):
|
def agent_recovery_email_task(pk: int) -> str:
|
||||||
|
from alerts.models import Alert
|
||||||
|
|
||||||
sleep(random.randint(1, 15))
|
sleep(random.randint(1, 15))
|
||||||
outage = AgentOutage.objects.get(pk=pk)
|
alert = Alert.objects.get(pk=pk)
|
||||||
outage.send_outage_email()
|
alert.agent.send_recovery_email()
|
||||||
outage.outage_email_sent = True
|
alert.resolved_email_sent = djangotime.now()
|
||||||
outage.save(update_fields=["outage_email_sent"])
|
alert.save(update_fields=["resolved_email_sent"])
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def agent_recovery_email_task(pk):
|
def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||||
sleep(random.randint(1, 15))
|
from alerts.models import Alert
|
||||||
outage = AgentOutage.objects.get(pk=pk)
|
|
||||||
outage.send_recovery_email()
|
alert = Alert.objects.get(pk=pk)
|
||||||
outage.recovery_email_sent = True
|
|
||||||
outage.save(update_fields=["recovery_email_sent"])
|
if not alert.sms_sent:
|
||||||
|
sleep(random.randint(1, 15))
|
||||||
|
alert.agent.send_outage_sms()
|
||||||
|
alert.sms_sent = djangotime.now()
|
||||||
|
alert.save(update_fields=["sms_sent"])
|
||||||
|
else:
|
||||||
|
if alert_interval:
|
||||||
|
# send an sms only if the last sms sent is older than alert interval
|
||||||
|
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||||
|
if alert.sms_sent < delta:
|
||||||
|
sleep(random.randint(1, 10))
|
||||||
|
alert.agent.send_outage_sms()
|
||||||
|
alert.sms_sent = djangotime.now()
|
||||||
|
alert.save(update_fields=["sms_sent"])
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def agent_outage_sms_task(pk):
|
def agent_recovery_sms_task(pk: int) -> str:
|
||||||
|
from alerts.models import Alert
|
||||||
|
|
||||||
sleep(random.randint(1, 3))
|
sleep(random.randint(1, 3))
|
||||||
outage = AgentOutage.objects.get(pk=pk)
|
alert = Alert.objects.get(pk=pk)
|
||||||
outage.send_outage_sms()
|
alert.agent.send_recovery_sms()
|
||||||
outage.outage_sms_sent = True
|
alert.resolved_sms_sent = djangotime.now()
|
||||||
outage.save(update_fields=["outage_sms_sent"])
|
alert.save(update_fields=["resolved_sms_sent"])
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def agent_recovery_sms_task(pk):
|
def agent_outages_task() -> None:
|
||||||
sleep(random.randint(1, 3))
|
from alerts.models import Alert
|
||||||
outage = AgentOutage.objects.get(pk=pk)
|
|
||||||
outage.send_recovery_sms()
|
|
||||||
outage.recovery_sms_sent = True
|
|
||||||
outage.save(update_fields=["recovery_sms_sent"])
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def agent_outages_task():
|
|
||||||
agents = Agent.objects.only(
|
agents = Agent.objects.only(
|
||||||
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
|
"pk",
|
||||||
|
"agent_id",
|
||||||
|
"last_seen",
|
||||||
|
"offline_time",
|
||||||
|
"overdue_time",
|
||||||
|
"overdue_email_alert",
|
||||||
|
"overdue_text_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
)
|
)
|
||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
if agent.status == "overdue":
|
||||||
if agent.status == "overdue":
|
Alert.handle_alert_failure(agent)
|
||||||
outages = AgentOutage.objects.filter(agent=agent)
|
|
||||||
if outages and outages.last().is_active:
|
|
||||||
continue
|
|
||||||
|
|
||||||
outage = AgentOutage(agent=agent)
|
|
||||||
outage.save()
|
|
||||||
|
|
||||||
if agent.overdue_email_alert and not agent.maintenance_mode:
|
@app.task
|
||||||
agent_outage_email_task.delay(pk=outage.pk)
|
def run_script_email_results_task(
|
||||||
|
agentpk: int,
|
||||||
|
scriptpk: int,
|
||||||
|
nats_timeout: int,
|
||||||
|
emails: list[str],
|
||||||
|
args: list[str] = [],
|
||||||
|
history_pk: int = 0,
|
||||||
|
):
|
||||||
|
agent = Agent.objects.get(pk=agentpk)
|
||||||
|
script = Script.objects.get(pk=scriptpk)
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=script.pk,
|
||||||
|
args=args,
|
||||||
|
full=True,
|
||||||
|
timeout=nats_timeout,
|
||||||
|
wait=True,
|
||||||
|
history_pk=history_pk,
|
||||||
|
)
|
||||||
|
if r == "timeout":
|
||||||
|
DebugLog.error(
|
||||||
|
agent=agent,
|
||||||
|
log_type="scripting",
|
||||||
|
message=f"{agent.hostname}({agent.pk}) timed out running script.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if agent.overdue_text_alert and not agent.maintenance_mode:
|
CORE = CoreSettings.objects.first()
|
||||||
agent_outage_sms_task.delay(pk=outage.pk)
|
subject = f"{agent.hostname} {script.name} Results"
|
||||||
|
exec_time = "{:.4f}".format(r["execution_time"])
|
||||||
|
body = (
|
||||||
|
subject
|
||||||
|
+ f"\nReturn code: {r['retcode']}\nExecution time: {exec_time} seconds\nStdout: {r['stdout']}\nStderr: {r['stderr']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
from email.message import EmailMessage
|
||||||
|
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = CORE.smtp_from_email # type:ignore
|
||||||
|
|
||||||
|
if emails:
|
||||||
|
msg["To"] = ", ".join(emails)
|
||||||
|
else:
|
||||||
|
msg["To"] = ", ".join(CORE.email_alert_recipients) # type:ignore
|
||||||
|
|
||||||
|
msg.set_content(body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP(
|
||||||
|
CORE.smtp_host, CORE.smtp_port, timeout=20 # type:ignore
|
||||||
|
) as server: # type:ignore
|
||||||
|
if CORE.smtp_requires_auth: # type:ignore
|
||||||
|
server.ehlo()
|
||||||
|
server.starttls()
|
||||||
|
server.login(
|
||||||
|
CORE.smtp_host_user, CORE.smtp_host_password # type:ignore
|
||||||
|
) # type:ignore
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
else:
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
except Exception as e:
|
||||||
|
DebugLog.error(message=e)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def clear_faults_task(older_than_days: int) -> None:
|
||||||
|
# https://github.com/wh1te909/tacticalrmm/issues/484
|
||||||
|
agents = Agent.objects.exclude(last_seen__isnull=True).filter(
|
||||||
|
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
|
||||||
|
)
|
||||||
|
for agent in agents:
|
||||||
|
if agent.agentchecks.exists():
|
||||||
|
for check in agent.agentchecks.all():
|
||||||
|
# reset check status
|
||||||
|
check.status = "passing"
|
||||||
|
check.save(update_fields=["status"])
|
||||||
|
if check.alert.filter(resolved=False).exists():
|
||||||
|
check.alert.get(resolved=False).resolve()
|
||||||
|
|
||||||
|
# reset overdue alerts
|
||||||
|
agent.overdue_email_alert = False
|
||||||
|
agent.overdue_text_alert = False
|
||||||
|
agent.overdue_dashboard_alert = False
|
||||||
|
agent.save(
|
||||||
|
update_fields=[
|
||||||
|
"overdue_email_alert",
|
||||||
|
"overdue_text_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def get_wmi_task() -> None:
|
||||||
|
agents = Agent.objects.only(
|
||||||
|
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
ids = [i.agent_id for i in agents if i.status == "online"]
|
||||||
|
run_nats_api_cmd("wmi", ids, timeout=45)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def agent_checkin_task() -> None:
|
||||||
|
run_nats_api_cmd("checkin", timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def agent_getinfo_task() -> None:
|
||||||
|
run_nats_api_cmd("agentinfo", timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def prune_agent_history(older_than_days: int) -> str:
|
||||||
|
from .models import AgentHistory
|
||||||
|
|
||||||
|
AgentHistory.objects.filter(
|
||||||
|
time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def handle_agents_task() -> None:
|
||||||
|
q = Agent.objects.prefetch_related("pendingactions", "autotasks").only(
|
||||||
|
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
agents = [
|
||||||
|
i
|
||||||
|
for i in q
|
||||||
|
if pyver.parse(i.version) >= pyver.parse("1.6.0") and i.status == "online"
|
||||||
|
]
|
||||||
|
for agent in agents:
|
||||||
|
# change agent update pending status to completed if agent has just updated
|
||||||
|
if (
|
||||||
|
pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
|
and agent.pendingactions.filter(
|
||||||
|
action_type="agentupdate", status="pending"
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
agent.pendingactions.filter(
|
||||||
|
action_type="agentupdate", status="pending"
|
||||||
|
).update(status="completed")
|
||||||
|
|
||||||
|
# sync scheduled tasks
|
||||||
|
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
|
||||||
|
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
if task.sync_status == "pendingdeletion":
|
||||||
|
task.delete_task_on_agent()
|
||||||
|
elif task.sync_status == "initial":
|
||||||
|
task.modify_task_on_agent()
|
||||||
|
elif task.sync_status == "notsynced":
|
||||||
|
task.create_task_on_agent()
|
||||||
|
|
||||||
|
# handles any alerting actions
|
||||||
|
if Alert.objects.filter(agent=agent, resolved=False).exists():
|
||||||
|
try:
|
||||||
|
Alert.handle_alert_resolve(agent)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("listagents/", views.AgentsTableList.as_view()),
|
path("listagents/", views.AgentsTableList.as_view()),
|
||||||
path("listagentsnodetail/", views.list_agents_no_detail),
|
path("listagentsnodetail/", views.list_agents_no_detail),
|
||||||
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
|
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
|
||||||
path("byclient/<int:clientpk>/", views.by_client),
|
|
||||||
path("bysite/<int:sitepk>/", views.by_site),
|
|
||||||
path("overdueaction/", views.overdue_action),
|
path("overdueaction/", views.overdue_action),
|
||||||
path("sendrawcmd/", views.send_raw_cmd),
|
path("sendrawcmd/", views.send_raw_cmd),
|
||||||
path("<pk>/agentdetail/", views.agent_detail),
|
path("<pk>/agentdetail/", views.agent_detail),
|
||||||
@@ -28,6 +27,7 @@ urlpatterns = [
|
|||||||
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
|
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
|
||||||
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
|
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
|
||||||
path("bulk/", views.bulk),
|
path("bulk/", views.bulk),
|
||||||
path("agent_counts/", views.agent_counts),
|
|
||||||
path("maintenance/", views.agent_maintenance),
|
path("maintenance/", views.agent_maintenance),
|
||||||
|
path("<int:pk>/wmi/", views.WMI.as_view()),
|
||||||
|
path("history/<int:pk>/", views.AgentHistoryView.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
37
api/tacticalrmm/agents/utils.py
Normal file
37
api/tacticalrmm/agents/utils.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import random
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_exegen_url() -> str:
|
||||||
|
urls: list[str] = settings.EXE_GEN_URLS
|
||||||
|
for url in urls:
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=10)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
return url
|
||||||
|
|
||||||
|
return random.choice(urls)
|
||||||
|
|
||||||
|
|
||||||
|
def get_winagent_url(arch: str) -> str:
|
||||||
|
from core.models import CodeSignToken
|
||||||
|
|
||||||
|
try:
|
||||||
|
codetoken = CodeSignToken.objects.first().token
|
||||||
|
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||||
|
params = {
|
||||||
|
"version": settings.LATEST_AGENT_VER,
|
||||||
|
"arch": arch,
|
||||||
|
"token": codetoken,
|
||||||
|
}
|
||||||
|
dl_url = base_url + urllib.parse.urlencode(params)
|
||||||
|
except:
|
||||||
|
dl_url = settings.DL_64 if arch == "64" else settings.DL_32
|
||||||
|
|
||||||
|
return dl_url
|
||||||
@@ -1,49 +1,60 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from loguru import logger
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
import pytz
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import datetime as dt
|
import time
|
||||||
from packaging import version as pyver
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import api_view
|
from packaging import version as pyver
|
||||||
from rest_framework.views import APIView
|
from rest_framework import status
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status, generics
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
from logs.models import AuditLog, DebugLog, PendingAction
|
||||||
from scripts.models import Script
|
from scripts.models import Script
|
||||||
from logs.models import AuditLog, PendingAction
|
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
|
||||||
|
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
|
||||||
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||||
|
|
||||||
|
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
|
||||||
|
from .permissions import (
|
||||||
|
EditAgentPerms,
|
||||||
|
EvtLogPerms,
|
||||||
|
InstallAgentPerms,
|
||||||
|
ManageNotesPerms,
|
||||||
|
ManageProcPerms,
|
||||||
|
MeshPerms,
|
||||||
|
RebootAgentPerms,
|
||||||
|
RunBulkPerms,
|
||||||
|
RunScriptPerms,
|
||||||
|
SendCMDPerms,
|
||||||
|
UninstallPerms,
|
||||||
|
UpdateAgentPerms,
|
||||||
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
AgentSerializer,
|
AgentCustomFieldSerializer,
|
||||||
AgentHostnameSerializer,
|
|
||||||
AgentTableSerializer,
|
|
||||||
AgentEditSerializer,
|
AgentEditSerializer,
|
||||||
|
AgentHistorySerializer,
|
||||||
|
AgentHostnameSerializer,
|
||||||
|
AgentOverdueActionSerializer,
|
||||||
|
AgentSerializer,
|
||||||
|
AgentTableSerializer,
|
||||||
NoteSerializer,
|
NoteSerializer,
|
||||||
NotesSerializer,
|
NotesSerializer,
|
||||||
)
|
)
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from .tasks import run_script_email_results_task, send_agent_update_task
|
||||||
|
|
||||||
from .tasks import uninstall_agent_task, send_agent_update_task
|
|
||||||
from winupdate.tasks import bulk_check_for_updates_task
|
|
||||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
|
|
||||||
|
|
||||||
from tacticalrmm.utils import notify_error, reload_nats
|
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
def get_agent_versions(request):
|
def get_agent_versions(request):
|
||||||
agents = Agent.objects.only("pk")
|
agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"versions": [settings.LATEST_AGENT_VER],
|
"versions": [settings.LATEST_AGENT_VER],
|
||||||
@@ -53,86 +64,115 @@ def get_agent_versions(request):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated, UpdateAgentPerms])
|
||||||
def update_agents(request):
|
def update_agents(request):
|
||||||
pks = request.data["pks"]
|
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
||||||
version = request.data["version"]
|
pks: list[int] = [
|
||||||
send_agent_update_task.delay(pks=pks, version=version)
|
i.pk
|
||||||
|
for i in q
|
||||||
|
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
|
]
|
||||||
|
send_agent_update_task.delay(pks=pks)
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
|
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||||
def ping(request, pk):
|
def ping(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
status = "offline"
|
status = "offline"
|
||||||
if agent.has_nats:
|
attempts = 0
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
while 1:
|
||||||
|
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||||
if r == "pong":
|
if r == "pong":
|
||||||
status = "online"
|
status = "online"
|
||||||
else:
|
break
|
||||||
r = agent.salt_api_cmd(timeout=5, func="test.ping")
|
else:
|
||||||
if isinstance(r, bool) and r:
|
attempts += 1
|
||||||
status = "online"
|
time.sleep(1)
|
||||||
|
|
||||||
|
if attempts >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
return Response({"name": agent.hostname, "status": status})
|
return Response({"name": agent.hostname, "status": status})
|
||||||
|
|
||||||
|
|
||||||
@api_view(["DELETE"])
|
@api_view(["DELETE"])
|
||||||
|
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||||
def uninstall(request):
|
def uninstall(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if agent.has_nats:
|
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
|
||||||
|
|
||||||
salt_id = agent.salt_id
|
|
||||||
name = agent.hostname
|
name = agent.hostname
|
||||||
has_nats = agent.has_nats
|
|
||||||
agent.delete()
|
agent.delete()
|
||||||
reload_nats()
|
reload_nats()
|
||||||
|
|
||||||
uninstall_agent_task.delay(salt_id, has_nats)
|
|
||||||
return Response(f"{name} will now be uninstalled.")
|
return Response(f"{name} will now be uninstalled.")
|
||||||
|
|
||||||
|
|
||||||
@api_view(["PATCH"])
|
@api_view(["PATCH", "PUT"])
|
||||||
|
@permission_classes([IsAuthenticated, EditAgentPerms])
|
||||||
def edit_agent(request):
|
def edit_agent(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||||
|
|
||||||
old_site = agent.site.pk
|
a_serializer = AgentEditSerializer(instance=agent, data=request.data, partial=True)
|
||||||
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
|
|
||||||
a_serializer.is_valid(raise_exception=True)
|
a_serializer.is_valid(raise_exception=True)
|
||||||
a_serializer.save()
|
a_serializer.save()
|
||||||
|
|
||||||
policy = agent.winupdatepolicy.get()
|
if "winupdatepolicy" in request.data.keys():
|
||||||
p_serializer = WinUpdatePolicySerializer(
|
policy = agent.winupdatepolicy.get() # type: ignore
|
||||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
p_serializer = WinUpdatePolicySerializer(
|
||||||
)
|
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||||
p_serializer.is_valid(raise_exception=True)
|
)
|
||||||
p_serializer.save()
|
p_serializer.is_valid(raise_exception=True)
|
||||||
|
p_serializer.save()
|
||||||
|
|
||||||
# check if site changed and initiate generating correct policies
|
if "custom_fields" in request.data.keys():
|
||||||
if old_site != request.data["site"]:
|
|
||||||
agent.generate_checks_from_policies(clear=True)
|
for field in request.data["custom_fields"]:
|
||||||
agent.generate_tasks_from_policies(clear=True)
|
|
||||||
|
custom_field = field
|
||||||
|
custom_field["agent"] = agent.id # type: ignore
|
||||||
|
|
||||||
|
if AgentCustomField.objects.filter(
|
||||||
|
field=field["field"], agent=agent.id # type: ignore
|
||||||
|
):
|
||||||
|
value = AgentCustomField.objects.get(
|
||||||
|
field=field["field"], agent=agent.id # type: ignore
|
||||||
|
)
|
||||||
|
serializer = AgentCustomFieldSerializer(
|
||||||
|
instance=value, data=custom_field
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
else:
|
||||||
|
serializer = AgentCustomFieldSerializer(data=custom_field)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
|
@permission_classes([IsAuthenticated, MeshPerms])
|
||||||
def meshcentral(request, pk):
|
def meshcentral(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
core = CoreSettings.objects.first()
|
core = CoreSettings.objects.first()
|
||||||
|
|
||||||
token = agent.get_login_token(
|
token = agent.get_login_token(
|
||||||
key=core.mesh_token, user=f"user//{core.mesh_username}"
|
key=core.mesh_token, user=f"user//{core.mesh_username}" # type:ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
if token == "err":
|
if token == "err":
|
||||||
return notify_error("Invalid mesh token")
|
return notify_error("Invalid mesh token")
|
||||||
|
|
||||||
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
|
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
|
||||||
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
|
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
|
||||||
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31"
|
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
|
||||||
|
|
||||||
AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname)
|
AuditLog.audit_mesh_session(
|
||||||
|
username=request.user.username,
|
||||||
|
agent=agent,
|
||||||
|
debug_info={"ip": request._client_ip},
|
||||||
|
)
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"hostname": agent.hostname,
|
"hostname": agent.hostname,
|
||||||
@@ -155,21 +195,16 @@ def agent_detail(request, pk):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def get_processes(request, pk):
|
def get_processes(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||||
if r == "timeout":
|
if r == "timeout":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
|
|
||||||
return Response(r)
|
return Response(r)
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
|
@permission_classes([IsAuthenticated, ManageProcPerms])
|
||||||
def kill_proc(request, pk, pid):
|
def kill_proc(request, pk, pid):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(
|
r = asyncio.run(
|
||||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||||
)
|
)
|
||||||
@@ -183,19 +218,19 @@ def kill_proc(request, pk, pid):
|
|||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
|
@permission_classes([IsAuthenticated, EvtLogPerms])
|
||||||
def get_event_log(request, pk, logtype, days):
|
def get_event_log(request, pk, logtype, days):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
timeout = 180 if logtype == "Security" else 30
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
data = {
|
data = {
|
||||||
"func": "eventlog",
|
"func": "eventlog",
|
||||||
"timeout": 30,
|
"timeout": timeout,
|
||||||
"payload": {
|
"payload": {
|
||||||
"logname": logtype,
|
"logname": logtype,
|
||||||
"days": str(days),
|
"days": str(days),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=32))
|
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
||||||
if r == "timeout":
|
if r == "timeout":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
|
|
||||||
@@ -203,10 +238,9 @@ def get_event_log(request, pk, logtype, days):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated, SendCMDPerms])
|
||||||
def send_raw_cmd(request):
|
def send_raw_cmd(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
timeout = int(request.data["timeout"])
|
timeout = int(request.data["timeout"])
|
||||||
data = {
|
data = {
|
||||||
"func": "rawcmd",
|
"func": "rawcmd",
|
||||||
@@ -216,6 +250,16 @@ def send_raw_cmd(request):
|
|||||||
"shell": request.data["shell"],
|
"shell": request.data["shell"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
|
||||||
|
hist = AgentHistory.objects.create(
|
||||||
|
agent=agent,
|
||||||
|
type="cmd_run",
|
||||||
|
command=request.data["cmd"],
|
||||||
|
username=request.user.username[:50],
|
||||||
|
)
|
||||||
|
data["id"] = hist.pk
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
||||||
|
|
||||||
if r == "timeout":
|
if r == "timeout":
|
||||||
@@ -223,44 +267,58 @@ def send_raw_cmd(request):
|
|||||||
|
|
||||||
AuditLog.audit_raw_command(
|
AuditLog.audit_raw_command(
|
||||||
username=request.user.username,
|
username=request.user.username,
|
||||||
hostname=agent.hostname,
|
agent=agent,
|
||||||
cmd=request.data["cmd"],
|
cmd=request.data["cmd"],
|
||||||
shell=request.data["shell"],
|
shell=request.data["shell"],
|
||||||
|
debug_info={"ip": request._client_ip},
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(r)
|
return Response(r)
|
||||||
|
|
||||||
|
|
||||||
class AgentsTableList(generics.ListAPIView):
|
class AgentsTableList(APIView):
|
||||||
queryset = (
|
def patch(self, request):
|
||||||
Agent.objects.select_related("site")
|
if "sitePK" in request.data.keys():
|
||||||
.prefetch_related("agentchecks")
|
queryset = (
|
||||||
.only(
|
Agent.objects.select_related("site", "policy", "alert_template")
|
||||||
|
.prefetch_related("agentchecks")
|
||||||
|
.filter(site_id=request.data["sitePK"])
|
||||||
|
)
|
||||||
|
elif "clientPK" in request.data.keys():
|
||||||
|
queryset = (
|
||||||
|
Agent.objects.select_related("site", "policy", "alert_template")
|
||||||
|
.prefetch_related("agentchecks")
|
||||||
|
.filter(site__client_id=request.data["clientPK"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = Agent.objects.select_related(
|
||||||
|
"site", "policy", "alert_template"
|
||||||
|
).prefetch_related("agentchecks")
|
||||||
|
|
||||||
|
queryset = queryset.only(
|
||||||
"pk",
|
"pk",
|
||||||
"hostname",
|
"hostname",
|
||||||
"agent_id",
|
"agent_id",
|
||||||
"site",
|
"site",
|
||||||
|
"policy",
|
||||||
|
"alert_template",
|
||||||
"monitoring_type",
|
"monitoring_type",
|
||||||
"description",
|
"description",
|
||||||
"needs_reboot",
|
"needs_reboot",
|
||||||
"overdue_text_alert",
|
"overdue_text_alert",
|
||||||
"overdue_email_alert",
|
"overdue_email_alert",
|
||||||
"overdue_time",
|
"overdue_time",
|
||||||
|
"offline_time",
|
||||||
"last_seen",
|
"last_seen",
|
||||||
"boot_time",
|
"boot_time",
|
||||||
"logged_in_username",
|
"logged_in_username",
|
||||||
"last_logged_in_user",
|
"last_logged_in_user",
|
||||||
"time_zone",
|
"time_zone",
|
||||||
"maintenance_mode",
|
"maintenance_mode",
|
||||||
|
"pending_actions_count",
|
||||||
|
"has_patches_pending",
|
||||||
)
|
)
|
||||||
)
|
ctx = {"default_tz": get_default_timezone()}
|
||||||
serializer_class = AgentTableSerializer
|
|
||||||
|
|
||||||
def list(self, request):
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
ctx = {
|
|
||||||
"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)
|
|
||||||
}
|
|
||||||
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
|
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -277,96 +335,22 @@ def agent_edit_details(request, pk):
|
|||||||
return Response(AgentEditSerializer(agent).data)
|
return Response(AgentEditSerializer(agent).data)
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
|
||||||
def by_client(request, clientpk):
|
|
||||||
agents = (
|
|
||||||
Agent.objects.select_related("site")
|
|
||||||
.filter(site__client_id=clientpk)
|
|
||||||
.prefetch_related("agentchecks")
|
|
||||||
.only(
|
|
||||||
"pk",
|
|
||||||
"hostname",
|
|
||||||
"agent_id",
|
|
||||||
"site",
|
|
||||||
"monitoring_type",
|
|
||||||
"description",
|
|
||||||
"needs_reboot",
|
|
||||||
"overdue_text_alert",
|
|
||||||
"overdue_email_alert",
|
|
||||||
"overdue_time",
|
|
||||||
"last_seen",
|
|
||||||
"boot_time",
|
|
||||||
"logged_in_username",
|
|
||||||
"last_logged_in_user",
|
|
||||||
"time_zone",
|
|
||||||
"maintenance_mode",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
|
|
||||||
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
|
||||||
def by_site(request, sitepk):
|
|
||||||
agents = (
|
|
||||||
Agent.objects.filter(site_id=sitepk)
|
|
||||||
.select_related("site")
|
|
||||||
.prefetch_related("agentchecks")
|
|
||||||
.only(
|
|
||||||
"pk",
|
|
||||||
"hostname",
|
|
||||||
"agent_id",
|
|
||||||
"site",
|
|
||||||
"monitoring_type",
|
|
||||||
"description",
|
|
||||||
"needs_reboot",
|
|
||||||
"overdue_text_alert",
|
|
||||||
"overdue_email_alert",
|
|
||||||
"overdue_time",
|
|
||||||
"last_seen",
|
|
||||||
"boot_time",
|
|
||||||
"logged_in_username",
|
|
||||||
"last_logged_in_user",
|
|
||||||
"time_zone",
|
|
||||||
"maintenance_mode",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
|
|
||||||
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def overdue_action(request):
|
def overdue_action(request):
|
||||||
pk = request.data["pk"]
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
alert_type = request.data["alertType"]
|
serializer = AgentOverdueActionSerializer(
|
||||||
action = request.data["action"]
|
instance=agent, data=request.data, partial=True
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
)
|
||||||
if alert_type == "email" and action == "enabled":
|
serializer.is_valid(raise_exception=True)
|
||||||
agent.overdue_email_alert = True
|
serializer.save()
|
||||||
agent.save(update_fields=["overdue_email_alert"])
|
|
||||||
elif alert_type == "email" and action == "disabled":
|
|
||||||
agent.overdue_email_alert = False
|
|
||||||
agent.save(update_fields=["overdue_email_alert"])
|
|
||||||
elif alert_type == "text" and action == "enabled":
|
|
||||||
agent.overdue_text_alert = True
|
|
||||||
agent.save(update_fields=["overdue_text_alert"])
|
|
||||||
elif alert_type == "text" and action == "disabled":
|
|
||||||
agent.overdue_text_alert = False
|
|
||||||
agent.save(update_fields=["overdue_text_alert"])
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
return Response(agent.hostname)
|
return Response(agent.hostname)
|
||||||
|
|
||||||
|
|
||||||
class Reboot(APIView):
|
class Reboot(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, RebootAgentPerms]
|
||||||
# reboot now
|
# reboot now
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
@@ -376,8 +360,6 @@ class Reboot(APIView):
|
|||||||
# reboot later
|
# reboot later
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_gotasks:
|
|
||||||
return notify_error("Requires agent version 1.1.1 or greater")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
||||||
@@ -392,6 +374,7 @@ class Reboot(APIView):
|
|||||||
"func": "schedtask",
|
"func": "schedtask",
|
||||||
"schedtaskpayload": {
|
"schedtaskpayload": {
|
||||||
"type": "schedreboot",
|
"type": "schedreboot",
|
||||||
|
"deleteafter": True,
|
||||||
"trigger": "once",
|
"trigger": "once",
|
||||||
"name": task_name,
|
"name": task_name,
|
||||||
"year": int(dt.datetime.strftime(obj, "%Y")),
|
"year": int(dt.datetime.strftime(obj, "%Y")),
|
||||||
@@ -417,8 +400,12 @@ class Reboot(APIView):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated, InstallAgentPerms])
|
||||||
def install_agent(request):
|
def install_agent(request):
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
from agents.utils import get_winagent_url
|
||||||
|
|
||||||
client_id = request.data["client"]
|
client_id = request.data["client"]
|
||||||
site_id = request.data["site"]
|
site_id = request.data["site"]
|
||||||
@@ -440,131 +427,29 @@ def install_agent(request):
|
|||||||
inno = (
|
inno = (
|
||||||
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
|
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
|
||||||
)
|
)
|
||||||
download_url = settings.DL_64 if arch == "64" else settings.DL_32
|
download_url = get_winagent_url(arch)
|
||||||
|
|
||||||
|
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||||
|
|
||||||
_, token = AuthToken.objects.create(
|
_, token = AuthToken.objects.create(
|
||||||
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
|
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.data["installMethod"] == "exe":
|
if request.data["installMethod"] == "exe":
|
||||||
go_bin = "/usr/local/rmmgo/go/bin/go"
|
from tacticalrmm.utils import generate_winagent_exe
|
||||||
|
|
||||||
if not os.path.exists(go_bin):
|
return generate_winagent_exe(
|
||||||
return Response("nogolang", status=status.HTTP_409_CONFLICT)
|
client=client_id,
|
||||||
|
site=site_id,
|
||||||
api = request.data["api"]
|
agent_type=request.data["agenttype"],
|
||||||
atype = request.data["agenttype"]
|
rdp=request.data["rdp"],
|
||||||
rdp = request.data["rdp"]
|
ping=request.data["ping"],
|
||||||
ping = request.data["ping"]
|
power=request.data["power"],
|
||||||
power = request.data["power"]
|
arch=arch,
|
||||||
|
token=token,
|
||||||
file_name = "rmm-installer.exe"
|
api=request.data["api"],
|
||||||
exe = os.path.join(settings.EXE_DIR, file_name)
|
file_name=request.data["fileName"],
|
||||||
|
)
|
||||||
if os.path.exists(exe):
|
|
||||||
try:
|
|
||||||
os.remove(exe)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(str(e))
|
|
||||||
|
|
||||||
goarch = "amd64" if arch == "64" else "386"
|
|
||||||
cmd = [
|
|
||||||
"env",
|
|
||||||
"GOOS=windows",
|
|
||||||
f"GOARCH={goarch}",
|
|
||||||
go_bin,
|
|
||||||
"build",
|
|
||||||
f"-ldflags=\"-X 'main.Inno={inno}'",
|
|
||||||
f"-X 'main.Api={api}'",
|
|
||||||
f"-X 'main.Client={client_id}'",
|
|
||||||
f"-X 'main.Site={site_id}'",
|
|
||||||
f"-X 'main.Atype={atype}'",
|
|
||||||
f"-X 'main.Rdp={rdp}'",
|
|
||||||
f"-X 'main.Ping={ping}'",
|
|
||||||
f"-X 'main.Power={power}'",
|
|
||||||
f"-X 'main.DownloadUrl={download_url}'",
|
|
||||||
f"-X 'main.Token={token}'\"",
|
|
||||||
"-o",
|
|
||||||
exe,
|
|
||||||
]
|
|
||||||
|
|
||||||
build_error = False
|
|
||||||
gen_error = False
|
|
||||||
|
|
||||||
gen = [
|
|
||||||
"env",
|
|
||||||
"GOOS=windows",
|
|
||||||
f"GOARCH={goarch}",
|
|
||||||
go_bin,
|
|
||||||
"generate",
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
r1 = subprocess.run(
|
|
||||||
" ".join(gen),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
gen_error = True
|
|
||||||
logger.error(str(e))
|
|
||||||
return Response(
|
|
||||||
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
|
|
||||||
)
|
|
||||||
|
|
||||||
if r1.returncode != 0:
|
|
||||||
gen_error = True
|
|
||||||
if r1.stdout:
|
|
||||||
logger.error(r1.stdout.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
if r1.stderr:
|
|
||||||
logger.error(r1.stderr.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
logger.error(f"Go build failed with return code {r1.returncode}")
|
|
||||||
|
|
||||||
if gen_error:
|
|
||||||
return Response(
|
|
||||||
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
" ".join(cmd),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
build_error = True
|
|
||||||
logger.error(str(e))
|
|
||||||
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
|
|
||||||
|
|
||||||
if r.returncode != 0:
|
|
||||||
build_error = True
|
|
||||||
if r.stdout:
|
|
||||||
logger.error(r.stdout.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
if r.stderr:
|
|
||||||
logger.error(r.stderr.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
logger.error(f"Go build failed with return code {r.returncode}")
|
|
||||||
|
|
||||||
if build_error:
|
|
||||||
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
with open(exe, "rb") as f:
|
|
||||||
response = HttpResponse(
|
|
||||||
f.read(),
|
|
||||||
content_type="application/vnd.microsoft.portable-executable",
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = f"inline; filename={file_name}"
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
response = HttpResponse()
|
|
||||||
response["Content-Disposition"] = f"attachment; filename={file_name}"
|
|
||||||
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
|
|
||||||
return response
|
|
||||||
|
|
||||||
elif request.data["installMethod"] == "manual":
|
elif request.data["installMethod"] == "manual":
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -572,12 +457,10 @@ def install_agent(request):
|
|||||||
"/VERYSILENT",
|
"/VERYSILENT",
|
||||||
"/SUPPRESSMSGBOXES",
|
"/SUPPRESSMSGBOXES",
|
||||||
"&&",
|
"&&",
|
||||||
"timeout",
|
"ping",
|
||||||
"/t",
|
"127.0.0.1",
|
||||||
"10",
|
"-n",
|
||||||
"/nobreak",
|
"5",
|
||||||
">",
|
|
||||||
"NUL",
|
|
||||||
"&&",
|
"&&",
|
||||||
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
|
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
|
||||||
"-m",
|
"-m",
|
||||||
@@ -604,8 +487,6 @@ def install_agent(request):
|
|||||||
resp = {
|
resp = {
|
||||||
"cmd": " ".join(str(i) for i in cmd),
|
"cmd": " ".join(str(i) for i in cmd),
|
||||||
"url": download_url,
|
"url": download_url,
|
||||||
"salt64": settings.SALT_64,
|
|
||||||
"salt32": settings.SALT_32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(resp)
|
return Response(resp)
|
||||||
@@ -640,7 +521,7 @@ def install_agent(request):
|
|||||||
try:
|
try:
|
||||||
os.remove(ps1)
|
os.remove(ps1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
DebugLog.error(message=str(e))
|
||||||
|
|
||||||
with open(ps1, "w") as f:
|
with open(ps1, "w") as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
@@ -662,27 +543,14 @@ def recover(request):
|
|||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
mode = request.data["mode"]
|
mode = request.data["mode"]
|
||||||
|
|
||||||
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
|
# attempt a realtime recovery, otherwise fall back to old recovery method
|
||||||
return notify_error("Only available in agent version greater than 0.9.5")
|
if mode == "tacagent" or mode == "mesh":
|
||||||
|
data = {"func": "recover", "payload": {"mode": mode}}
|
||||||
|
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
||||||
|
if r == "ok":
|
||||||
|
return Response("Successfully completed recovery")
|
||||||
|
|
||||||
if not agent.has_nats:
|
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
|
||||||
if mode == "tacagent" or mode == "checkrunner" or mode == "rpc":
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
|
|
||||||
if agent.has_nats:
|
|
||||||
if (
|
|
||||||
mode == "tacagent"
|
|
||||||
or mode == "checkrunner"
|
|
||||||
or mode == "salt"
|
|
||||||
or mode == "mesh"
|
|
||||||
):
|
|
||||||
data = {"func": "recover", "payload": {"mode": mode}}
|
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
|
||||||
if r == "ok":
|
|
||||||
return Response("Successfully completed recovery")
|
|
||||||
|
|
||||||
if agent.recoveryactions.filter(last_run=None).exists():
|
|
||||||
return notify_error(
|
return notify_error(
|
||||||
"A recovery action is currently pending. Please wait for the next agent check-in."
|
"A recovery action is currently pending. Please wait for the next agent check-in."
|
||||||
)
|
)
|
||||||
@@ -708,46 +576,106 @@ def recover(request):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated, RunScriptPerms])
|
||||||
def run_script(request):
|
def run_script(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
script = get_object_or_404(Script, pk=request.data["script"])
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
|
||||||
output = request.data["output"]
|
output = request.data["output"]
|
||||||
|
args = request.data["args"]
|
||||||
req_timeout = int(request.data["timeout"]) + 3
|
req_timeout = int(request.data["timeout"]) + 3
|
||||||
|
|
||||||
AuditLog.audit_script_run(
|
AuditLog.audit_script_run(
|
||||||
username=request.user.username,
|
username=request.user.username,
|
||||||
hostname=agent.hostname,
|
agent=agent,
|
||||||
script=script.name,
|
script=script.name,
|
||||||
|
debug_info={"ip": request._client_ip},
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
history_pk = 0
|
||||||
"func": "runscript",
|
if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
|
||||||
"timeout": request.data["timeout"],
|
hist = AgentHistory.objects.create(
|
||||||
"script_args": request.data["args"],
|
agent=agent,
|
||||||
"payload": {
|
type="script_run",
|
||||||
"code": script.code,
|
script=script,
|
||||||
"shell": script.shell,
|
username=request.user.username[:50],
|
||||||
},
|
)
|
||||||
}
|
history_pk = hist.pk
|
||||||
|
|
||||||
if output == "wait":
|
if output == "wait":
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout))
|
r = agent.run_script(
|
||||||
|
scriptpk=script.pk,
|
||||||
|
args=args,
|
||||||
|
timeout=req_timeout,
|
||||||
|
wait=True,
|
||||||
|
history_pk=history_pk,
|
||||||
|
)
|
||||||
|
return Response(r)
|
||||||
|
|
||||||
|
elif output == "email":
|
||||||
|
emails = (
|
||||||
|
[] if request.data["emailMode"] == "default" else request.data["emails"]
|
||||||
|
)
|
||||||
|
run_script_email_results_task.delay(
|
||||||
|
agentpk=agent.pk,
|
||||||
|
scriptpk=script.pk,
|
||||||
|
nats_timeout=req_timeout,
|
||||||
|
emails=emails,
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
elif output == "collector":
|
||||||
|
from core.models import CustomField
|
||||||
|
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=script.pk,
|
||||||
|
args=args,
|
||||||
|
timeout=req_timeout,
|
||||||
|
wait=True,
|
||||||
|
history_pk=history_pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
custom_field = CustomField.objects.get(pk=request.data["custom_field"])
|
||||||
|
|
||||||
|
if custom_field.model == "agent":
|
||||||
|
field = custom_field.get_or_create_field_value(agent)
|
||||||
|
elif custom_field.model == "client":
|
||||||
|
field = custom_field.get_or_create_field_value(agent.client)
|
||||||
|
elif custom_field.model == "site":
|
||||||
|
field = custom_field.get_or_create_field_value(agent.site)
|
||||||
|
else:
|
||||||
|
return notify_error("Custom Field was invalid")
|
||||||
|
|
||||||
|
value = (
|
||||||
|
r.strip()
|
||||||
|
if request.data["save_all_output"]
|
||||||
|
else r.strip().split("\n")[-1].strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
field.save_to_field(value)
|
||||||
|
return Response(r)
|
||||||
|
elif output == "note":
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=script.pk,
|
||||||
|
args=args,
|
||||||
|
timeout=req_timeout,
|
||||||
|
wait=True,
|
||||||
|
history_pk=history_pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
Note.objects.create(agent=agent, user=request.user, note=r)
|
||||||
return Response(r)
|
return Response(r)
|
||||||
else:
|
else:
|
||||||
asyncio.run(agent.nats_cmd(data, wait=False))
|
agent.run_script(
|
||||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
def recover_mesh(request, pk):
|
def recover_mesh(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
r = asyncio.run(agent.nats_cmd(data, timeout=90))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
|
|
||||||
@@ -789,6 +717,8 @@ class GetAddNotes(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class GetEditDeleteNote(APIView):
|
class GetEditDeleteNote(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, ManageNotesPerms]
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
note = get_object_or_404(Note, pk=pk)
|
note = get_object_or_404(Note, pk=pk)
|
||||||
return Response(NoteSerializer(note).data)
|
return Response(NoteSerializer(note).data)
|
||||||
@@ -807,8 +737,9 @@ class GetEditDeleteNote(APIView):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated, RunBulkPerms])
|
||||||
def bulk(request):
|
def bulk(request):
|
||||||
if request.data["target"] == "agents" and not request.data["agentPKs"]:
|
if request.data["target"] == "agents" and not request.data["agents"]:
|
||||||
return notify_error("Must select at least 1 agent")
|
return notify_error("Must select at least 1 agent")
|
||||||
|
|
||||||
if request.data["target"] == "client":
|
if request.data["target"] == "client":
|
||||||
@@ -816,64 +747,65 @@ def bulk(request):
|
|||||||
elif request.data["target"] == "site":
|
elif request.data["target"] == "site":
|
||||||
q = Agent.objects.filter(site_id=request.data["site"])
|
q = Agent.objects.filter(site_id=request.data["site"])
|
||||||
elif request.data["target"] == "agents":
|
elif request.data["target"] == "agents":
|
||||||
q = Agent.objects.filter(pk__in=request.data["agentPKs"])
|
q = Agent.objects.filter(pk__in=request.data["agents"])
|
||||||
elif request.data["target"] == "all":
|
elif request.data["target"] == "all":
|
||||||
q = Agent.objects.all()
|
q = Agent.objects.only("pk", "monitoring_type")
|
||||||
else:
|
else:
|
||||||
return notify_error("Something went wrong")
|
return notify_error("Something went wrong")
|
||||||
|
|
||||||
minions = [agent.salt_id for agent in q]
|
if request.data["monType"] == "servers":
|
||||||
agents = [agent.pk for agent in q]
|
q = q.filter(monitoring_type="server")
|
||||||
|
elif request.data["monType"] == "workstations":
|
||||||
|
q = q.filter(monitoring_type="workstation")
|
||||||
|
|
||||||
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
|
agents: list[int] = [agent.pk for agent in q]
|
||||||
|
|
||||||
|
if not agents:
|
||||||
|
return notify_error("No agents where found meeting the selected criteria")
|
||||||
|
|
||||||
|
AuditLog.audit_bulk_action(
|
||||||
|
request.user,
|
||||||
|
request.data["mode"],
|
||||||
|
request.data,
|
||||||
|
debug_info={"ip": request._client_ip},
|
||||||
|
)
|
||||||
|
|
||||||
if request.data["mode"] == "command":
|
if request.data["mode"] == "command":
|
||||||
handle_bulk_command_task.delay(
|
handle_bulk_command_task.delay(
|
||||||
agents, request.data["cmd"], request.data["shell"], request.data["timeout"]
|
agents,
|
||||||
|
request.data["cmd"],
|
||||||
|
request.data["shell"],
|
||||||
|
request.data["timeout"],
|
||||||
|
request.user.username[:50],
|
||||||
|
run_on_offline=request.data["offlineAgents"],
|
||||||
)
|
)
|
||||||
return Response(f"Command will now be run on {len(agents)} agents")
|
return Response(f"Command will now be run on {len(agents)} agents")
|
||||||
|
|
||||||
elif request.data["mode"] == "script":
|
elif request.data["mode"] == "script":
|
||||||
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
script = get_object_or_404(Script, pk=request.data["script"])
|
||||||
handle_bulk_script_task.delay(
|
handle_bulk_script_task.delay(
|
||||||
script.pk, agents, request.data["args"], request.data["timeout"]
|
script.pk,
|
||||||
|
agents,
|
||||||
|
request.data["args"],
|
||||||
|
request.data["timeout"],
|
||||||
|
request.user.username[:50],
|
||||||
)
|
)
|
||||||
return Response(f"{script.name} will now be run on {len(agents)} agents")
|
return Response(f"{script.name} will now be run on {len(agents)} agents")
|
||||||
|
|
||||||
elif request.data["mode"] == "install":
|
elif request.data["mode"] == "patch":
|
||||||
r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates")
|
|
||||||
if r == "timeout":
|
if request.data["patchMode"] == "install":
|
||||||
return notify_error("Salt API not running")
|
bulk_install_updates_task.delay(agents)
|
||||||
return Response(
|
return Response(
|
||||||
f"Pending updates will now be installed on {len(agents)} agents"
|
f"Pending updates will now be installed on {len(agents)} agents"
|
||||||
)
|
)
|
||||||
elif request.data["mode"] == "scan":
|
elif request.data["patchMode"] == "scan":
|
||||||
bulk_check_for_updates_task.delay(minions=minions)
|
bulk_check_for_updates_task.delay(agents)
|
||||||
return Response(f"Patch status scan will now run on {len(agents)} agents")
|
return Response(f"Patch status scan will now run on {len(agents)} agents")
|
||||||
|
|
||||||
return notify_error("Something went wrong")
|
return notify_error("Something went wrong")
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
|
||||||
def agent_counts(request):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"total_server_count": Agent.objects.filter(
|
|
||||||
monitoring_type="server"
|
|
||||||
).count(),
|
|
||||||
"total_server_offline_count": AgentOutage.objects.filter(
|
|
||||||
recovery_time=None, agent__monitoring_type="server"
|
|
||||||
).count(),
|
|
||||||
"total_workstation_count": Agent.objects.filter(
|
|
||||||
monitoring_type="workstation"
|
|
||||||
).count(),
|
|
||||||
"total_workstation_offline_count": AgentOutage.objects.filter(
|
|
||||||
recovery_time=None, agent__monitoring_type="workstation"
|
|
||||||
).count(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def agent_maintenance(request):
|
def agent_maintenance(request):
|
||||||
if request.data["type"] == "Client":
|
if request.data["type"] == "Client":
|
||||||
@@ -895,3 +827,20 @@ def agent_maintenance(request):
|
|||||||
return notify_error("Invalid data")
|
return notify_error("Invalid data")
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class WMI(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
|
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
|
||||||
|
if r != "ok":
|
||||||
|
return notify_error("Unable to contact the agent")
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentHistoryView(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
|
history = AgentHistory.objects.filter(agent=agent)
|
||||||
|
|
||||||
|
return Response(AgentHistorySerializer(history, many=True).data)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Alert
|
from .models import Alert, AlertTemplate
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Alert)
|
admin.site.register(Alert)
|
||||||
|
admin.site.register(AlertTemplate)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.1 on 2020-08-15 15:31
|
# Generated by Django 3.1 on 2020-08-15 15:31
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -42,4 +42,4 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -27,4 +27,4 @@ class Migration(migrations.Migration):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -1,25 +1,31 @@
|
|||||||
# Generated by Django 3.1.2 on 2020-10-21 18:15
|
# Generated by Django 3.1.2 on 2020-10-21 18:15
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('checks', '0010_auto_20200922_1344'),
|
("checks", "0010_auto_20200922_1344"),
|
||||||
('alerts', '0002_auto_20200815_1618'),
|
("alerts", "0002_auto_20200815_1618"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='alert',
|
model_name="alert",
|
||||||
name='assigned_check',
|
name="assigned_check",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='checks.check'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="alert",
|
||||||
|
to="checks.check",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='alert',
|
model_name="alert",
|
||||||
name='alert_time',
|
name="alert_time",
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
172
api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
Normal file
172
api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-02-12 14:08
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0029_delete_agentoutage'),
|
||||||
|
('clients', '0008_auto_20201103_1430'),
|
||||||
|
('autotasks', '0017_auto_20210210_1512'),
|
||||||
|
('scripts', '0005_auto_20201207_1606'),
|
||||||
|
('alerts', '0003_auto_20201021_1815'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_execution_time',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_retcode',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_run',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_stderr',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_stdout',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_timeout',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='alert_type',
|
||||||
|
field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='assigned_task',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='email_sent',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='hidden',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_execution_time',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_retcode',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_run',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_stderr',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_stdout',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_timeout',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_email_sent',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_on',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_sms_sent',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='sms_sent',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alert',
|
||||||
|
name='snoozed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alert',
|
||||||
|
name='severity',
|
||||||
|
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AlertTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||||
|
('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||||
|
('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
|
||||||
|
('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
|
||||||
|
('email_from', models.EmailField(blank=True, max_length=254, null=True)),
|
||||||
|
('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||||
|
('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('check_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('check_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('check_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||||
|
('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||||
|
('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('task_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('task_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('task_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||||
|
('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||||
|
('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')),
|
||||||
|
('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')),
|
||||||
|
('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')),
|
||||||
|
('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')),
|
||||||
|
('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
31
api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
Normal file
31
api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-02-12 17:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0004_auto_20210212_1408'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='alert',
|
||||||
|
name='action_timeout',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='alert',
|
||||||
|
name='resolved_action_timeout',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='action_timeout',
|
||||||
|
field=models.PositiveIntegerField(default=15),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='resolved_action_timeout',
|
||||||
|
field=models.PositiveIntegerField(default=15),
|
||||||
|
),
|
||||||
|
]
|
||||||
72
api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
Normal file
72
api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-17 17:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0005_auto_20210212_1745'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_include_desktops',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='exclude_servers',
|
||||||
|
field=models.BooleanField(blank=True, default=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='exclude_workstations',
|
||||||
|
field=models.BooleanField(blank=True, default=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_always_alert',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_always_email',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_always_text',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='check_always_alert',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='check_always_email',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='check_always_text',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='task_always_alert',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='task_always_email',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='task_always_text',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
33
api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
Normal file
33
api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-21 04:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0006_auto_20210217_1736'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='created_by',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='created_time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='modified_time',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
Normal file
28
api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-21 17:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0007_auto_20210721_0423'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='check_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='task_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
Normal file
28
api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.2.1 on 2021-07-21 18:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0008_auto_20210721_1757'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='agent_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='check_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alerttemplate',
|
||||||
|
name='task_script_actions',
|
||||||
|
field=models.BooleanField(blank=True, default=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user