Compare commits
1145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
ff69bed394 | ||
|
|
d6e8c5146f | ||
|
|
9a04cf99d7 | ||
|
|
86e7c11e71 | ||
|
|
361cc08faa | ||
|
|
70dc771052 | ||
|
|
c14873a799 | ||
|
|
bba5abd74b | ||
|
|
a224e79c1f | ||
|
|
c305d98186 | ||
|
|
7c5a473e71 | ||
|
|
5e0f5d1eed | ||
|
|
238b269bc4 | ||
|
|
0ad121b9d2 | ||
|
|
7088acd9fd | ||
|
|
e0a900d4b6 | ||
|
|
a0fe2f0c7d | ||
|
|
d5b9bc2f26 | ||
|
|
584254e6ca | ||
|
|
a2963ed7bb | ||
|
|
2a3c2e133d | ||
|
|
3e7dcb2755 | ||
|
|
faeec00b39 | ||
|
|
eeed81392f | ||
|
|
95dce9e992 | ||
|
|
502bd2a191 | ||
|
|
17ac92a9d0 | ||
|
|
ba028cde0c | ||
|
|
6e751e7a9b | ||
|
|
948b56d0e6 | ||
|
|
4bf2dc9ece | ||
|
|
125823f8ab | ||
|
|
24d33397e9 | ||
|
|
2c553825f4 | ||
|
|
198c485e9a | ||
|
|
0138505507 | ||
|
|
5d50dcc600 | ||
|
|
7bdd8c4626 | ||
|
|
fc82c35f0c | ||
|
|
426ebad300 | ||
|
|
1afe61c593 | ||
|
|
c20751829b | ||
|
|
a3b8ee8392 | ||
|
|
156c0fe7f6 | ||
|
|
216f7a38cf | ||
|
|
fd04dc10d4 | ||
|
|
d39bdce926 | ||
|
|
c6e01245b0 | ||
|
|
c168ee7ba4 | ||
|
|
7575253000 | ||
|
|
c28c1efbb1 | ||
|
|
e6aa2c3b78 | ||
|
|
ab7c481f83 | ||
|
|
84ad1c352d | ||
|
|
e9aad39ac9 | ||
|
|
c3444a87bc | ||
|
|
67b224b340 | ||
|
|
bded14d36b | ||
|
|
73fa0b6631 | ||
|
|
2f07337588 | ||
|
|
da163d44e7 | ||
|
|
56fbf8ae0c | ||
|
|
327eb4b39b | ||
|
|
ae7873a7e3 | ||
|
|
9a5f01813b | ||
|
|
0605a3b725 | ||
|
|
09c535f159 | ||
|
|
7fb11da5df | ||
|
|
9c9a46499a | ||
|
|
6fca60261e | ||
|
|
00537b32ef | ||
|
|
8636758a90 | ||
|
|
e39dfbd624 | ||
|
|
6e048b2a12 | ||
|
|
f9657599c2 | ||
|
|
42ae3bba9b | ||
|
|
2fd56a4bfe | ||
|
|
824bcc5603 | ||
|
|
4fbb613aaa | ||
|
|
9eb45270f2 | ||
|
|
75c61c53e8 | ||
|
|
2688a47436 | ||
|
|
fe3bf4b189 | ||
|
|
456cb5ebb2 | ||
|
|
3d91d574b4 | ||
|
|
54876c5499 | ||
|
|
d256585284 | ||
|
|
bd8f100b43 | ||
|
|
44f05f2dcc | ||
|
|
43f7f82bdc | ||
|
|
e902f63211 | ||
|
|
129f68e194 | ||
|
|
4b37fe12d7 | ||
|
|
6de79922c5 | ||
|
|
e1a9791f44 | ||
|
|
81795f51c6 | ||
|
|
68dfb11155 | ||
|
|
39fc1beb89 | ||
|
|
fe0ddec0f9 | ||
|
|
9b52b4efd9 | ||
|
|
e90e527603 | ||
|
|
a510854741 | ||
|
|
8935ce4ccf | ||
|
|
f9edc9059a | ||
|
|
db8917a769 | ||
|
|
c2d70cc1c2 | ||
|
|
3b13c7f9ce | ||
|
|
b7150d8026 | ||
|
|
041830a7f8 | ||
|
|
a18daf0195 | ||
|
|
5d3dfceb22 | ||
|
|
c82855e732 | ||
|
|
956f156018 | ||
|
|
9b13c35e7f | ||
|
|
bc8e637bba | ||
|
|
f03c28c906 | ||
|
|
e4b1f39fdc | ||
|
|
4780af910c | ||
|
|
d61ce5c524 | ||
|
|
20ab151f4d | ||
|
|
8a7be7543a | ||
|
|
3f806aec9c | ||
|
|
6c273b32bb | ||
|
|
b986f9d6ee | ||
|
|
c98cca6b7b | ||
|
|
fbec78ede5 | ||
|
|
c1d9a2d1f1 | ||
|
|
8a10036f32 | ||
|
|
924a3aec0e | ||
|
|
3b3ac31541 | ||
|
|
e0cb2f9d0f | ||
|
|
549b4edb59 | ||
|
|
67c912aca2 | ||
|
|
a74dde5d9e | ||
|
|
f7bcd24726 | ||
|
|
337c900770 | ||
|
|
e83e73ead4 | ||
|
|
24f6f9b063 | ||
|
|
5dc999360e | ||
|
|
9ec2f6b64d | ||
|
|
f970592efe | ||
|
|
7592c11e99 | ||
|
|
759b05e137 | ||
|
|
42ebd9ffce | ||
|
|
bc0fc33966 | ||
|
|
f4aab16e39 | ||
|
|
e91425287c | ||
|
|
f05908f570 | ||
|
|
8b351edf9c | ||
|
|
93c06eaba0 | ||
|
|
a8d9fa75d4 | ||
|
|
159ecd3e4f | ||
|
|
717803c665 | ||
|
|
0d40589e8a | ||
|
|
8c5544bfad | ||
|
|
0c9be9f84f | ||
|
|
497729ecd6 | ||
|
|
21a8efa3b8 | ||
|
|
c2f942a51e | ||
|
|
63b4b95240 | ||
|
|
955f37e005 | ||
|
|
cd2ae89b0e | ||
|
|
0b013fa438 | ||
|
|
478b657354 | ||
|
|
65b6aabe69 | ||
|
|
3fabae5b5f | ||
|
|
96c46a9e12 | ||
|
|
381b93e8eb | ||
|
|
f51e5b6fbf | ||
|
|
20befd1ca2 | ||
|
|
ac6c6130f8 | ||
|
|
d776a2325c | ||
|
|
4aec4257da | ||
|
|
d654f856d1 | ||
|
|
8d3b0a2069 | ||
|
|
54a96f35e8 | ||
|
|
2dc56d72f6 | ||
|
|
4b6ddb535a | ||
|
|
697e2250d4 | ||
|
|
6a75035b04 | ||
|
|
46b166bc41 | ||
|
|
6bbc0987ad | ||
|
|
8c480b43e2 | ||
|
|
079f6731dd | ||
|
|
f99d5754cd | ||
|
|
bf8c41e362 | ||
|
|
7f7bc06eb4 | ||
|
|
b507e59359 | ||
|
|
72078ac6bf | ||
|
|
0db9e082e2 | ||
|
|
0c44394a76 | ||
|
|
e20aa0cf04 | ||
|
|
fa30a50a91 | ||
|
|
f6629ff12c | ||
|
|
4128e4db73 | ||
|
|
34cac5685f | ||
|
|
4c9b91d536 | ||
|
|
95b95a8998 | ||
|
|
617738bb28 | ||
|
|
f6ac15d790 | ||
|
|
79e1324ead | ||
|
|
4ef9f010f0 | ||
|
|
e6e8865708 | ||
|
|
33cd8f9b0d | ||
|
|
a7138e019c | ||
|
|
049b72bd50 | ||
|
|
f3f1987515 | ||
|
|
a9395d89cd | ||
|
|
bc2fcee8ba | ||
|
|
242ff2ceca | ||
|
|
70790ac762 | ||
|
|
0f98869b61 | ||
|
|
9ddc02140f | ||
|
|
ee631b3d20 | ||
|
|
32f56e60d8 | ||
|
|
6102b51d9e | ||
|
|
2baee27859 | ||
|
|
144a3dedbb | ||
|
|
f90d966f1a | ||
|
|
b188e2ea97 |
28
.devcontainer/.env.example
Normal file
28
.devcontainer/.env.example
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
|
HTTP_PROTOCOL=https
|
||||||
24
.devcontainer/api.dockerfile
Normal file
24
.devcontainer/api.dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM python:3.9.2-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 Dev python reqs
|
||||||
|
COPY ./requirements.txt /
|
||||||
|
|
||||||
|
# Copy Docker Entrypoint
|
||||||
|
COPY ./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
|
||||||
257
.devcontainer/docker-compose.yml
Normal file
257
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api-dev:
|
||||||
|
container_name: trmm-api-dev
|
||||||
|
image: api-dev
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./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:
|
||||||
|
- "4222:4222"
|
||||||
|
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: 172.21.0.20
|
||||||
|
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
|
||||||
|
image: redis:6.0-alpine
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-redis
|
||||||
|
|
||||||
|
init-dev:
|
||||||
|
container_name: trmm-init-dev
|
||||||
|
image: api-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
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
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
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
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
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
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
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}
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
ipv4_address: 172.21.0.20
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
|
||||||
|
mkdocs-dev:
|
||||||
|
container_name: trmm-mkdocs-dev
|
||||||
|
image: api-dev
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
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:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 172.21.0.0/24
|
||||||
177
.devcontainer/entrypoint.sh
Normal file
177
.devcontainer/entrypoint.sh
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/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}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DATETIME_FORMAT': '%b-%d-%Y - %H:%M',
|
||||||
|
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'knox.auth.TokenAuthentication',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not DEBUG:
|
||||||
|
REST_FRAMEWORK.update({
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
35
.devcontainer/requirements.txt
Normal file
35
.devcontainer/requirements.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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
|
||||||
|
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
|
||||||
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
**/__pycache__
|
||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/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
|
||||||
78
.github/workflows/docker-build-push.yml
vendored
Normal file
78
.github/workflows/docker-build-push.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: Publish Tactical Docker Images
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Build and Push Docker Images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get Github Tag
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
echo ::set-output name=version::${GITHUB_REF#refs/tags/v}
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Tactical Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: ./docker/containers/tactical/dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},tacticalrmm/tactical:latest
|
||||||
|
|
||||||
|
- name: Build and Push Tactical MeshCentral Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: ./docker/containers/tactical-meshcentral/dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},tacticalrmm/tactical-meshcentral:latest
|
||||||
|
|
||||||
|
- name: Build and Push Tactical NATS Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: ./docker/containers/tactical-nats/dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
|
||||||
|
|
||||||
|
- name: Build and Push Tactical Frontend Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: ./docker/containers/tactical-frontend/dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},tacticalrmm/tactical-frontend:latest
|
||||||
|
|
||||||
|
- name: Build and Push Tactical Nginx Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: ./docker/containers/tactical-nginx/dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nginx:latest
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -34,6 +34,7 @@ app.ini
|
|||||||
create_services.py
|
create_services.py
|
||||||
gen_random.py
|
gen_random.py
|
||||||
sync_salt_modules.py
|
sync_salt_modules.py
|
||||||
|
change_times.py
|
||||||
rmm-*.exe
|
rmm-*.exe
|
||||||
rmm-*.ps1
|
rmm-*.ps1
|
||||||
api/tacticalrmm/accounts/management/commands/*.json
|
api/tacticalrmm/accounts/management/commands/*.json
|
||||||
@@ -41,3 +42,8 @@ api/tacticalrmm/accounts/management/commands/random_data.py
|
|||||||
versioninfo.go
|
versioninfo.go
|
||||||
resource.syso
|
resource.syso
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
docker-compose.dev.yml
|
||||||
|
docs/.vuepress/dist
|
||||||
|
nats-rmm.conf
|
||||||
|
.mypy_cache
|
||||||
|
docs/site/
|
||||||
|
|||||||
43
.travis.yml
43
.travis.yml
@@ -1,43 +0,0 @@
|
|||||||
dist: focal
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: node_js
|
|
||||||
node_js: "12"
|
|
||||||
before_install:
|
|
||||||
- cd web
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- npm run test:unit
|
|
||||||
|
|
||||||
- language: python
|
|
||||||
python: "3.8"
|
|
||||||
services:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
addons:
|
|
||||||
postgresql: "13"
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- postgresql-13
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- psql -c 'CREATE DATABASE travisci;' -U postgres
|
|
||||||
- psql -c "CREATE USER travisci WITH PASSWORD 'travisSuperSekret6645';" -U postgres
|
|
||||||
- psql -c 'GRANT ALL PRIVILEGES ON DATABASE travisci TO travisci;' -U postgres
|
|
||||||
- psql -c 'ALTER USER travisci CREATEDB;' -U postgres
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- cd api/tacticalrmm
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install --no-cache-dir --upgrade pip
|
|
||||||
- pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
|
|
||||||
- pip install --no-cache-dir -r requirements.txt -r requirements-test.txt
|
|
||||||
|
|
||||||
script:
|
|
||||||
- coverage run manage.py test -v 2
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- coveralls
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -2,8 +2,15 @@
|
|||||||
"python.pythonPath": "api/tacticalrmm/env/bin/python",
|
"python.pythonPath": "api/tacticalrmm/env/bin/python",
|
||||||
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
README.md
116
README.md
@@ -1,21 +1,20 @@
|
|||||||
# Tactical RMM
|
# Tactical RMM
|
||||||
|
|
||||||
[](https://travis-ci.com/wh1te909/tacticalrmm)
|
|
||||||
[](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
|
[](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
|
||||||
[](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
|
[](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](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. Alot 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
|
||||||
@@ -34,109 +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 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/develop/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](./update.sh) ([Raw](https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/update.sh))
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/update.sh
|
|
||||||
chmod +x update.sh
|
|
||||||
./update.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup
|
|
||||||
Download [backup.sh](./backup.sh) ([Raw](https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/backup.sh))
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/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/develop/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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using another ssl certificate
|
|
||||||
During the install you can opt out of using the Let's Encrypt certificate. If you do this the script will create a self-signed certificate, so that https continues to work. You can replace the certificates in /certs/example.com/(privkey.pem | pubkey.pem) with your own.
|
|
||||||
|
|
||||||
If you are migrating from Let's Encrypt to another certificate provider, you can create the /certs directory and copy your certificates there. It is recommended to do this because this directory will be backed up with the backup script provided. Then modify the nginx configurations to use your new certificates
|
|
||||||
|
|
||||||
The cert that is generated is a wildcard certificate and is used in the nginx configurations: rmm.conf, api.conf, and mesh.conf. If you can't generate wildcard certificates you can create a cert for each subdomain and configure each nginx configuration file to use its own certificate. Then restart nginx:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
```
|
|
||||||
@@ -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,5 +1,4 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
26
api/tacticalrmm/accounts/migrations/0006_user_agent.py
Normal file
26
api/tacticalrmm/accounts/migrations/0006_user_agent.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-10 20:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0024_auto_20201101_2319"),
|
||||||
|
("accounts", "0005_auto_20201002_1303"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="agent",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user",
|
||||||
|
to="agents.agent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-01 22:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def link_agents_to_users(apps, schema_editor):
|
||||||
|
Agent = apps.get_model("agents", "Agent")
|
||||||
|
User = apps.get_model("accounts", "User")
|
||||||
|
for agent in Agent.objects.all():
|
||||||
|
user = User.objects.filter(username=agent.agent_id).first()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
user.agent = agent
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0006_user_agent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(link_agents_to_users, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
18
api/tacticalrmm/accounts/migrations/0008_user_dark_mode.py
Normal file
18
api/tacticalrmm/accounts/migrations/0008_user_dark_mode.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2020-11-12 00:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0007_update_agent_primary_key"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="dark_mode",
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,12 +1,51 @@
|
|||||||
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 logs.models import BaseAuditModel
|
from logs.models import BaseAuditModel
|
||||||
|
|
||||||
|
AGENT_DBLCLICK_CHOICES = [
|
||||||
|
("editagent", "Edit Agent"),
|
||||||
|
("takecontrol", "Take Control"),
|
||||||
|
("remotebg", "Remote Background"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
||||||
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)
|
||||||
|
show_community_scripts = models.BooleanField(default=True)
|
||||||
|
agent_dblclick_action = models.CharField(
|
||||||
|
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
|
||||||
|
agent = models.OneToOneField(
|
||||||
|
"agents.Agent",
|
||||||
|
related_name="user",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serialize(user):
|
def serialize(user):
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import pyotp
|
import pyotp
|
||||||
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.serializers import (
|
|
||||||
ModelSerializer,
|
|
||||||
SerializerMethodField,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserUISerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"dark_mode",
|
||||||
|
"show_community_scripts",
|
||||||
|
"agent_dblclick_action",
|
||||||
|
"default_agent_tbl_tab",
|
||||||
|
"client_tree_sort",
|
||||||
|
"client_tree_splitter",
|
||||||
|
"loading_bar_color",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from tacticalrmm.test import TacticalTestCase
|
|
||||||
from accounts.models import User
|
from accounts.models import User
|
||||||
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAccounts(TacticalTestCase):
|
class TestAccounts(TacticalTestCase):
|
||||||
@@ -155,6 +156,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 +194,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 +225,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,6 +251,41 @@ class TestUserAction(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 = "/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/"
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
|
|
||||||
class TestTOTPSetup(TacticalTestCase):
|
class TestTOTPSetup(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -7,4 +8,5 @@ urlpatterns = [
|
|||||||
path("users/reset/", views.UserActions.as_view()),
|
path("users/reset/", views.UserActions.as_view()),
|
||||||
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()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
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 rest_framework.authtoken.serializers import AuthTokenSerializer
|
|
||||||
from knox.views import LoginView as KnoxLoginView
|
from knox.views import LoginView as KnoxLoginView
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from .models import User
|
|
||||||
from agents.models import Agent
|
|
||||||
from logs.models import AuditLog
|
from logs.models import AuditLog
|
||||||
from tacticalrmm.utils import notify_error
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .serializers import UserSerializer, TOTPSetupSerializer
|
from .models import User
|
||||||
|
from .serializers import 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):
|
||||||
@@ -60,7 +65,7 @@ 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:
|
||||||
@@ -74,15 +79,14 @@ class LoginView(KnoxLoginView):
|
|||||||
|
|
||||||
class GetAddUsers(APIView):
|
class GetAddUsers(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
agents = Agent.objects.values_list("agent_id", flat=True)
|
users = User.objects.filter(agent=None)
|
||||||
users = User.objects.exclude(username__in=agents)
|
|
||||||
|
|
||||||
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"],
|
||||||
@@ -109,6 +113,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()
|
||||||
@@ -116,7 +123,11 @@ 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")
|
||||||
|
|
||||||
@@ -125,8 +136,10 @@ class UserActions(APIView):
|
|||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
@@ -134,8 +147,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()
|
||||||
|
|
||||||
@@ -157,3 +172,13 @@ class TOTPSetup(APIView):
|
|||||||
return Response(TOTPSetupSerializer(user).data)
|
return Response(TOTPSetupSerializer(user).data)
|
||||||
|
|
||||||
return Response("totp token already set")
|
return Response("totp token already set")
|
||||||
|
|
||||||
|
|
||||||
|
class UserUI(APIView):
|
||||||
|
def patch(self, request):
|
||||||
|
serializer = UserUISerializer(
|
||||||
|
instance=request.user, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response("ok")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
from .models import Agent, AgentCustomField, Note, RecoveryAction
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
from .models import Agent
|
import json
|
||||||
from model_bakery.recipe import Recipe, seq
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
|
from model_bakery.recipe import Recipe, foreign_key, seq
|
||||||
|
|
||||||
|
|
||||||
|
def generate_agent_id(hostname):
|
||||||
|
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
|
||||||
|
return f"{rand}-{hostname}"
|
||||||
|
|
||||||
|
|
||||||
|
site = Recipe("clients.Site")
|
||||||
|
|
||||||
|
|
||||||
|
def get_wmi_data():
|
||||||
|
with open(
|
||||||
|
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
|
||||||
|
) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
agent = Recipe(
|
agent = Recipe(
|
||||||
Agent,
|
"agents.Agent",
|
||||||
client="Default",
|
site=foreign_key(site),
|
||||||
site="Default",
|
hostname="DESKTOP-TEST123",
|
||||||
hostname=seq("TestHostname"),
|
version="1.3.0",
|
||||||
monitoring_type=cycle(["workstation", "server"]),
|
monitoring_type=cycle(["workstation", "server"]),
|
||||||
|
agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
|
||||||
)
|
)
|
||||||
|
|
||||||
server_agent = agent.extend(
|
server_agent = agent.extend(
|
||||||
@@ -21,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(
|
||||||
@@ -49,3 +75,5 @@ agent_with_services = agent.extend(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
agent_with_wmi = agent.extend(wmi=get_wmi_data())
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
26
api/tacticalrmm/agents/migrations/0021_agent_site_link.py
Normal file
26
api/tacticalrmm/agents/migrations/0021_agent_site_link.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-01 22:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("clients", "0006_deployment"),
|
||||||
|
("agents", "0020_auto_20201025_2129"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="agent",
|
||||||
|
name="site_link",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="agents",
|
||||||
|
to="clients.site",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-01 22:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def link_sites_to_agents(apps, schema_editor):
|
||||||
|
Agent = apps.get_model("agents", "Agent")
|
||||||
|
Site = apps.get_model("clients", "Site")
|
||||||
|
for agent in Agent.objects.all():
|
||||||
|
site = Site.objects.get(client__client=agent.client, site=agent.site)
|
||||||
|
agent.site_link = site
|
||||||
|
agent.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
Agent = apps.get_model("agents", "Agent")
|
||||||
|
for agent in Agent.objects.all():
|
||||||
|
agent.site = agent.site_link.site
|
||||||
|
agent.client = agent.site_link.client.client
|
||||||
|
agent.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0021_agent_site_link"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(link_sites_to_agents, reverse),
|
||||||
|
]
|
||||||
21
api/tacticalrmm/agents/migrations/0023_auto_20201101_2312.py
Normal file
21
api/tacticalrmm/agents/migrations/0023_auto_20201101_2312.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-01 23:12
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0022_update_site_primary_key"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="agent",
|
||||||
|
name="client",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="agent",
|
||||||
|
name="site",
|
||||||
|
),
|
||||||
|
]
|
||||||
18
api/tacticalrmm/agents/migrations/0024_auto_20201101_2319.py
Normal file
18
api/tacticalrmm/agents/migrations/0024_auto_20201101_2319.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-11-01 23:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0023_auto_20201101_2312"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="agent",
|
||||||
|
old_name="site_link",
|
||||||
|
new_name="site",
|
||||||
|
),
|
||||||
|
]
|
||||||
27
api/tacticalrmm/agents/migrations/0025_auto_20201122_0407.py
Normal file
27
api/tacticalrmm/agents/migrations/0025_auto_20201122_0407.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2020-11-22 04:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0024_auto_20201101_2319"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="recoveryaction",
|
||||||
|
name="mode",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("salt", "Salt"),
|
||||||
|
("mesh", "Mesh"),
|
||||||
|
("command", "Command"),
|
||||||
|
("rpc", "Nats RPC"),
|
||||||
|
],
|
||||||
|
default="mesh",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
api/tacticalrmm/agents/migrations/0026_auto_20201125_2334.py
Normal file
28
api/tacticalrmm/agents/migrations/0026_auto_20201125_2334.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2020-11-25 23:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0025_auto_20201122_0407"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="recoveryaction",
|
||||||
|
name="mode",
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
import requests
|
import asyncio
|
||||||
import datetime as dt
|
|
||||||
import time
|
|
||||||
import base64
|
import base64
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from Crypto.Random import get_random_bytes
|
|
||||||
from Crypto.Hash import SHA3_384
|
|
||||||
from Crypto.Util.Padding import pad
|
|
||||||
import validators
|
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import string
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from loguru import logger
|
|
||||||
from packaging import version as pyver
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.db import models
|
import msgpack
|
||||||
|
import validators
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Hash import SHA3_384
|
||||||
|
from Crypto.Random import get_random_bytes
|
||||||
|
from Crypto.Util.Padding import pad
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db import models
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
|
from loguru import logger
|
||||||
|
from nats.aio.client import Client as NATS
|
||||||
|
from nats.aio.errors import ErrTimeout
|
||||||
|
|
||||||
from core.models import CoreSettings, TZ_CHOICES
|
from core.models import TZ_CHOICES, CoreSettings
|
||||||
from logs.models import BaseAuditModel
|
from logs.models import BaseAuditModel
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
@@ -44,14 +45,14 @@ class Agent(BaseAuditModel):
|
|||||||
boot_time = models.FloatField(null=True, blank=True)
|
boot_time = models.FloatField(null=True, blank=True)
|
||||||
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
|
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
|
||||||
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
|
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
|
||||||
client = models.CharField(max_length=200)
|
|
||||||
antivirus = models.CharField(default="n/a", max_length=255) # deprecated
|
antivirus = models.CharField(default="n/a", max_length=255) # deprecated
|
||||||
site = models.CharField(max_length=150)
|
|
||||||
monitoring_type = models.CharField(max_length=30)
|
monitoring_type = models.CharField(max_length=30)
|
||||||
description = models.CharField(null=True, blank=True, max_length=255)
|
description = models.CharField(null=True, blank=True, max_length=255)
|
||||||
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
|
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
|
||||||
overdue_email_alert = models.BooleanField(default=False)
|
overdue_email_alert = models.BooleanField(default=False)
|
||||||
overdue_text_alert = models.BooleanField(default=False)
|
overdue_text_alert = models.BooleanField(default=False)
|
||||||
|
overdue_dashboard_alert = models.BooleanField(default=False)
|
||||||
|
offline_time = models.PositiveIntegerField(default=4)
|
||||||
overdue_time = models.PositiveIntegerField(default=30)
|
overdue_time = models.PositiveIntegerField(default=30)
|
||||||
check_interval = models.PositiveIntegerField(default=120)
|
check_interval = models.PositiveIntegerField(default=120)
|
||||||
needs_reboot = models.BooleanField(default=False)
|
needs_reboot = models.BooleanField(default=False)
|
||||||
@@ -62,6 +63,21 @@ class Agent(BaseAuditModel):
|
|||||||
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
||||||
)
|
)
|
||||||
maintenance_mode = models.BooleanField(default=False)
|
maintenance_mode = models.BooleanField(default=False)
|
||||||
|
block_policy_inheritance = models.BooleanField(default=False)
|
||||||
|
alert_template = models.ForeignKey(
|
||||||
|
"alerts.AlertTemplate",
|
||||||
|
related_name="agents",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
"clients.Site",
|
||||||
|
related_name="agents",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
policy = models.ForeignKey(
|
policy = models.ForeignKey(
|
||||||
"automation.Policy",
|
"automation.Policy",
|
||||||
related_name="agents",
|
related_name="agents",
|
||||||
@@ -70,9 +86,31 @@ class Agent(BaseAuditModel):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# get old agent if exists
|
||||||
|
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||||
|
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
# check if new agent has been created
|
||||||
|
# or check if policy have changed on agent
|
||||||
|
# or if site has changed on agent and if so generate-policies
|
||||||
|
if (
|
||||||
|
not old_agent
|
||||||
|
or (old_agent and old_agent.policy != self.policy)
|
||||||
|
or (old_agent.site != self.site)
|
||||||
|
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
|
||||||
|
):
|
||||||
|
self.generate_checks_from_policies()
|
||||||
|
self.generate_tasks_from_policies()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.hostname
|
return self.hostname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
return self.site.client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self):
|
||||||
# return the default timezone unless the timezone is explicity set per agent
|
# return the default timezone unless the timezone is explicity set per agent
|
||||||
@@ -86,9 +124,9 @@ class Agent(BaseAuditModel):
|
|||||||
@property
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
if self.operating_system is not None:
|
if self.operating_system is not None:
|
||||||
if "64 bit" in self.operating_system:
|
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
|
||||||
return "64"
|
return "64"
|
||||||
elif "32 bit" in self.operating_system:
|
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
|
||||||
return "32"
|
return "32"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -100,14 +138,6 @@ class Agent(BaseAuditModel):
|
|||||||
return settings.DL_32
|
return settings.DL_32
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def winsalt_dl(self):
|
|
||||||
if self.arch == "64":
|
|
||||||
return settings.SALT_64
|
|
||||||
elif self.arch == "32":
|
|
||||||
return settings.SALT_32
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def win_inno_exe(self):
|
def win_inno_exe(self):
|
||||||
if self.arch == "64":
|
if self.arch == "64":
|
||||||
@@ -118,7 +148,7 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
offline = djangotime.now() - djangotime.timedelta(minutes=6)
|
offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
|
||||||
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
|
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
|
||||||
|
|
||||||
if self.last_seen is not None:
|
if self.last_seen is not None:
|
||||||
@@ -133,31 +163,32 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_patches_pending(self):
|
def has_patches_pending(self):
|
||||||
|
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
|
||||||
if self.winupdates.filter(action="approve").filter(installed=False).exists():
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def checks(self):
|
def checks(self):
|
||||||
total, passing, failing = 0, 0, 0
|
total, passing, failing, warning, info = 0, 0, 0, 0, 0
|
||||||
|
|
||||||
if self.agentchecks.exists():
|
if self.agentchecks.exists(): # type: ignore
|
||||||
for i in self.agentchecks.all():
|
for i in self.agentchecks.all(): # type: ignore
|
||||||
total += 1
|
total += 1
|
||||||
if i.status == "passing":
|
if i.status == "passing":
|
||||||
passing += 1
|
passing += 1
|
||||||
elif i.status == "failing":
|
elif i.status == "failing":
|
||||||
failing += 1
|
if i.alert_severity == "error":
|
||||||
|
failing += 1
|
||||||
has_failing_checks = True if failing > 0 else False
|
elif i.alert_severity == "warning":
|
||||||
|
warning += 1
|
||||||
|
elif i.alert_severity == "info":
|
||||||
|
info += 1
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"total": total,
|
"total": total,
|
||||||
"passing": passing,
|
"passing": passing,
|
||||||
"failing": failing,
|
"failing": failing,
|
||||||
"has_failing_checks": has_failing_checks,
|
"warning": warning,
|
||||||
|
"info": info,
|
||||||
|
"has_failing_checks": failing > 0 or warning > 0,
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -172,6 +203,27 @@ class Agent(BaseAuditModel):
|
|||||||
except:
|
except:
|
||||||
return ["unknown cpu model"]
|
return ["unknown cpu model"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def graphics(self):
|
||||||
|
ret, mrda = [], []
|
||||||
|
try:
|
||||||
|
graphics = self.wmi_detail["graphics"]
|
||||||
|
for i in graphics:
|
||||||
|
caption = [x["Caption"] for x in i if "Caption" in x][0]
|
||||||
|
if "microsoft remote display adapter" in caption.lower():
|
||||||
|
mrda.append("yes")
|
||||||
|
continue
|
||||||
|
|
||||||
|
ret.append([x["Caption"] for x in i if "Caption" in x][0])
|
||||||
|
|
||||||
|
# only return this if no other graphics cards
|
||||||
|
if not ret and mrda:
|
||||||
|
return "Microsoft Remote Display Adapter"
|
||||||
|
|
||||||
|
return ", ".join(ret)
|
||||||
|
except:
|
||||||
|
return "Graphics info requires agent v1.4.14"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_ips(self):
|
def local_ips(self):
|
||||||
ret = []
|
ret = []
|
||||||
@@ -216,6 +268,7 @@ class Agent(BaseAuditModel):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
|
||||||
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
|
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -245,33 +298,107 @@ class Agent(BaseAuditModel):
|
|||||||
except:
|
except:
|
||||||
return ["unknown disk"]
|
return ["unknown disk"]
|
||||||
|
|
||||||
|
def check_run_interval(self) -> int:
|
||||||
|
interval = self.check_interval
|
||||||
|
# determine if any agent checks have a custom interval and set the lowest interval
|
||||||
|
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
|
||||||
|
if check.run_interval and check.run_interval < interval:
|
||||||
|
|
||||||
|
# don't allow check runs less than 15s
|
||||||
|
if check.run_interval < 15:
|
||||||
|
interval = 15
|
||||||
|
else:
|
||||||
|
interval = check.run_interval
|
||||||
|
|
||||||
|
return interval
|
||||||
|
|
||||||
|
def run_script(
|
||||||
|
self,
|
||||||
|
scriptpk: int,
|
||||||
|
args: list[str] = [],
|
||||||
|
timeout: int = 120,
|
||||||
|
full: bool = False,
|
||||||
|
wait: bool = False,
|
||||||
|
run_on_any: bool = False,
|
||||||
|
) -> Any:
|
||||||
|
|
||||||
|
from scripts.models import Script
|
||||||
|
|
||||||
|
script = Script.objects.get(pk=scriptpk)
|
||||||
|
|
||||||
|
parsed_args = script.parse_script_args(self, script.shell, args)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"func": "runscriptfull" if full else "runscript",
|
||||||
|
"timeout": timeout,
|
||||||
|
"script_args": parsed_args,
|
||||||
|
"payload": {
|
||||||
|
"code": script.code,
|
||||||
|
"shell": script.shell,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
running_agent = self
|
||||||
|
if run_on_any:
|
||||||
|
nats_ping = {"func": "ping"}
|
||||||
|
|
||||||
|
# try on self first
|
||||||
|
r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
|
||||||
|
|
||||||
|
if r == "pong":
|
||||||
|
running_agent = self
|
||||||
|
else:
|
||||||
|
online = [
|
||||||
|
agent
|
||||||
|
for agent in Agent.objects.only(
|
||||||
|
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
if agent.status == "online"
|
||||||
|
]
|
||||||
|
|
||||||
|
for agent in online:
|
||||||
|
r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
|
||||||
|
if r == "pong":
|
||||||
|
running_agent = agent
|
||||||
|
break
|
||||||
|
|
||||||
|
if running_agent.pk == self.pk:
|
||||||
|
return "Unable to find an online agent"
|
||||||
|
|
||||||
|
if wait:
|
||||||
|
return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
|
||||||
|
else:
|
||||||
|
asyncio.run(running_agent.nats_cmd(data, wait=False))
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
# auto approves updates
|
# auto approves updates
|
||||||
def approve_updates(self):
|
def approve_updates(self):
|
||||||
patch_policy = self.get_patch_policy()
|
patch_policy = self.get_patch_policy()
|
||||||
|
|
||||||
updates = list()
|
updates = list()
|
||||||
if patch_policy.critical == "approve":
|
if patch_policy.critical == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Critical", installed=False
|
severity="Critical", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.important == "approve":
|
if patch_policy.important == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Important", installed=False
|
severity="Important", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.moderate == "approve":
|
if patch_policy.moderate == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Moderate", installed=False
|
severity="Moderate", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.low == "approve":
|
if patch_policy.low == "approve":
|
||||||
updates += self.winupdates.filter(severity="Low", installed=False).exclude(
|
updates += self.winupdates.filter(severity="Low", installed=False).exclude( # type: ignore
|
||||||
action="approve"
|
action="approve"
|
||||||
)
|
)
|
||||||
|
|
||||||
if patch_policy.other == "approve":
|
if patch_policy.other == "approve":
|
||||||
updates += self.winupdates.filter(severity="", installed=False).exclude(
|
updates += self.winupdates.filter(severity="", installed=False).exclude( # type: ignore
|
||||||
action="approve"
|
action="approve"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -281,14 +408,12 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
# returns agent policy merged with a client or site specific policy
|
# returns agent policy merged with a client or site specific policy
|
||||||
def get_patch_policy(self):
|
def get_patch_policy(self):
|
||||||
from clients.models import Client, Site
|
|
||||||
|
|
||||||
# check if site has a patch policy and if so use it
|
# check if site has a patch policy and if so use it
|
||||||
client = Client.objects.get(client=self.client)
|
site = self.site
|
||||||
site = Site.objects.get(client=client, site=self.site)
|
|
||||||
core_settings = CoreSettings.objects.first()
|
core_settings = CoreSettings.objects.first()
|
||||||
patch_policy = None
|
patch_policy = None
|
||||||
agent_policy = self.winupdatepolicy.get()
|
agent_policy = self.winupdatepolicy.get() # type: ignore
|
||||||
|
|
||||||
if self.monitoring_type == "server":
|
if self.monitoring_type == "server":
|
||||||
# check agent policy first which should override client or site policy
|
# check agent policy first which should override client or site policy
|
||||||
@@ -297,21 +422,34 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
# check site policy if agent policy doesn't have one
|
# check site policy if agent policy doesn't have one
|
||||||
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
|
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
|
||||||
patch_policy = site.server_policy.winupdatepolicy.get()
|
# make sure agent isn;t blocking policy inheritance
|
||||||
|
if not self.block_policy_inheritance:
|
||||||
|
patch_policy = site.server_policy.winupdatepolicy.get()
|
||||||
|
|
||||||
# if site doesn't have a patch policy check the client
|
# if site doesn't have a patch policy check the client
|
||||||
elif (
|
elif (
|
||||||
site.client.server_policy
|
site.client.server_policy
|
||||||
and site.client.server_policy.winupdatepolicy.exists()
|
and site.client.server_policy.winupdatepolicy.exists()
|
||||||
):
|
):
|
||||||
patch_policy = site.client.server_policy.winupdatepolicy.get()
|
# make sure agent and site are not blocking inheritance
|
||||||
|
if (
|
||||||
|
not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
):
|
||||||
|
patch_policy = site.client.server_policy.winupdatepolicy.get()
|
||||||
|
|
||||||
# if patch policy still doesn't exist check default policy
|
# if patch policy still doesn't exist check default policy
|
||||||
elif (
|
elif (
|
||||||
core_settings.server_policy
|
core_settings.server_policy
|
||||||
and core_settings.server_policy.winupdatepolicy.exists()
|
and core_settings.server_policy.winupdatepolicy.exists()
|
||||||
):
|
):
|
||||||
patch_policy = core_settings.server_policy.winupdatepolicy.get()
|
# make sure agent site and client are not blocking inheritance
|
||||||
|
if (
|
||||||
|
not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
and not site.client.block_policy_inheritance
|
||||||
|
):
|
||||||
|
patch_policy = core_settings.server_policy.winupdatepolicy.get()
|
||||||
|
|
||||||
elif self.monitoring_type == "workstation":
|
elif self.monitoring_type == "workstation":
|
||||||
# check agent policy first which should override client or site policy
|
# check agent policy first which should override client or site policy
|
||||||
@@ -322,21 +460,36 @@ class Agent(BaseAuditModel):
|
|||||||
site.workstation_policy
|
site.workstation_policy
|
||||||
and site.workstation_policy.winupdatepolicy.exists()
|
and site.workstation_policy.winupdatepolicy.exists()
|
||||||
):
|
):
|
||||||
patch_policy = site.workstation_policy.winupdatepolicy.get()
|
# make sure agent isn;t blocking policy inheritance
|
||||||
|
if not self.block_policy_inheritance:
|
||||||
|
patch_policy = site.workstation_policy.winupdatepolicy.get()
|
||||||
|
|
||||||
# if site doesn't have a patch policy check the client
|
# if site doesn't have a patch policy check the client
|
||||||
elif (
|
elif (
|
||||||
site.client.workstation_policy
|
site.client.workstation_policy
|
||||||
and site.client.workstation_policy.winupdatepolicy.exists()
|
and site.client.workstation_policy.winupdatepolicy.exists()
|
||||||
):
|
):
|
||||||
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
|
# make sure agent and site are not blocking inheritance
|
||||||
|
if (
|
||||||
|
not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
):
|
||||||
|
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
|
||||||
|
|
||||||
# if patch policy still doesn't exist check default policy
|
# if patch policy still doesn't exist check default policy
|
||||||
elif (
|
elif (
|
||||||
core_settings.workstation_policy
|
core_settings.workstation_policy
|
||||||
and core_settings.workstation_policy.winupdatepolicy.exists()
|
and core_settings.workstation_policy.winupdatepolicy.exists()
|
||||||
):
|
):
|
||||||
patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
|
# make sure agent site and client are not blocking inheritance
|
||||||
|
if (
|
||||||
|
not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
and not site.client.block_policy_inheritance
|
||||||
|
):
|
||||||
|
patch_policy = (
|
||||||
|
core_settings.workstation_policy.winupdatepolicy.get()
|
||||||
|
)
|
||||||
|
|
||||||
# if policy still doesn't exist return the agent patch policy
|
# if policy still doesn't exist return the agent patch policy
|
||||||
if not patch_policy:
|
if not patch_policy:
|
||||||
@@ -373,32 +526,162 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
return patch_policy
|
return patch_policy
|
||||||
|
|
||||||
# clear is used to delete managed policy checks from agent
|
def get_approved_update_guids(self) -> list[str]:
|
||||||
# parent_checks specifies a list of checks to delete from agent with matching parent_check field
|
return list(
|
||||||
def generate_checks_from_policies(self, clear=False):
|
self.winupdates.filter(action="approve", installed=False).values_list( # type: ignore
|
||||||
|
"guid", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# sets alert template assigned in the following order: policy, site, client, global
|
||||||
|
# sets None if nothing is found
|
||||||
|
def set_alert_template(self):
|
||||||
|
|
||||||
|
site = self.site
|
||||||
|
client = self.client
|
||||||
|
core = CoreSettings.objects.first()
|
||||||
|
|
||||||
|
templates = list()
|
||||||
|
# check if alert template is on a policy assigned to agent
|
||||||
|
if (
|
||||||
|
self.policy
|
||||||
|
and self.policy.alert_template
|
||||||
|
and self.policy.alert_template.is_active
|
||||||
|
):
|
||||||
|
templates.append(self.policy.alert_template)
|
||||||
|
|
||||||
|
# check if policy with alert template is assigned to the site
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "server"
|
||||||
|
and site.server_policy
|
||||||
|
and site.server_policy.alert_template
|
||||||
|
and site.server_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(site.server_policy.alert_template)
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "workstation"
|
||||||
|
and site.workstation_policy
|
||||||
|
and site.workstation_policy.alert_template
|
||||||
|
and site.workstation_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(site.workstation_policy.alert_template)
|
||||||
|
|
||||||
|
# check if alert template is assigned to site
|
||||||
|
if site.alert_template and site.alert_template.is_active:
|
||||||
|
templates.append(site.alert_template)
|
||||||
|
|
||||||
|
# check if policy with alert template is assigned to the client
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "server"
|
||||||
|
and client.server_policy
|
||||||
|
and client.server_policy.alert_template
|
||||||
|
and client.server_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(client.server_policy.alert_template)
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "workstation"
|
||||||
|
and client.workstation_policy
|
||||||
|
and client.workstation_policy.alert_template
|
||||||
|
and client.workstation_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(client.workstation_policy.alert_template)
|
||||||
|
|
||||||
|
# check if alert template is on client and return
|
||||||
|
if (
|
||||||
|
client.alert_template
|
||||||
|
and client.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(client.alert_template)
|
||||||
|
|
||||||
|
# check if alert template is applied globally and return
|
||||||
|
if (
|
||||||
|
core.alert_template
|
||||||
|
and core.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
and not client.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(core.alert_template)
|
||||||
|
|
||||||
|
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "server"
|
||||||
|
and core.server_policy
|
||||||
|
and core.server_policy.alert_template
|
||||||
|
and core.server_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
and not client.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(core.server_policy.alert_template)
|
||||||
|
if (
|
||||||
|
self.monitoring_type == "workstation"
|
||||||
|
and core.workstation_policy
|
||||||
|
and core.workstation_policy.alert_template
|
||||||
|
and core.workstation_policy.alert_template.is_active
|
||||||
|
and not self.block_policy_inheritance
|
||||||
|
and not site.block_policy_inheritance
|
||||||
|
and not client.block_policy_inheritance
|
||||||
|
):
|
||||||
|
templates.append(core.workstation_policy.alert_template)
|
||||||
|
|
||||||
|
# go through the templates and return the first one that isn't excluded
|
||||||
|
for template in templates:
|
||||||
|
# check if client, site, or agent has been excluded from template
|
||||||
|
if (
|
||||||
|
client.pk
|
||||||
|
in template.excluded_clients.all().values_list("pk", flat=True)
|
||||||
|
or site.pk in template.excluded_sites.all().values_list("pk", flat=True)
|
||||||
|
or self.pk
|
||||||
|
in template.excluded_agents.all()
|
||||||
|
.only("pk")
|
||||||
|
.values_list("pk", flat=True)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# check if template is excluding desktops
|
||||||
|
elif (
|
||||||
|
self.monitoring_type == "workstation" and template.exclude_workstations
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# check if template is excluding servers
|
||||||
|
elif self.monitoring_type == "server" and template.exclude_servers:
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# save alert_template to agent cache field
|
||||||
|
self.alert_template = template
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
# no alert templates found or agent has been excluded
|
||||||
|
self.alert_template = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_checks_from_policies(self):
|
||||||
from automation.models import Policy
|
from automation.models import Policy
|
||||||
|
|
||||||
# Clear agent checks managed by policy
|
|
||||||
if clear:
|
|
||||||
self.agentchecks.filter(managed_by_policy=True).delete()
|
|
||||||
|
|
||||||
# Clear agent checks that have overriden_by_policy set
|
# Clear agent checks that have overriden_by_policy set
|
||||||
self.agentchecks.update(overriden_by_policy=False)
|
self.agentchecks.update(overriden_by_policy=False) # type: ignore
|
||||||
|
|
||||||
# Generate checks based on policies
|
# Generate checks based on policies
|
||||||
Policy.generate_policy_checks(self)
|
Policy.generate_policy_checks(self)
|
||||||
|
|
||||||
# clear is used to delete managed policy tasks from agent
|
def generate_tasks_from_policies(self):
|
||||||
# parent_tasks specifies a list of tasks to delete from agent with matching parent_task field
|
|
||||||
def generate_tasks_from_policies(self, clear=False):
|
|
||||||
from autotasks.tasks import delete_win_task_schedule
|
|
||||||
from automation.models import Policy
|
from automation.models import Policy
|
||||||
|
|
||||||
# Clear agent tasks managed by policy
|
|
||||||
if clear:
|
|
||||||
for task in self.autotasks.filter(managed_by_policy=True):
|
|
||||||
delete_win_task_schedule.delay(task.pk)
|
|
||||||
|
|
||||||
# Generate tasks based on policies
|
# Generate tasks based on policies
|
||||||
Policy.generate_policy_tasks(self)
|
Policy.generate_policy_tasks(self)
|
||||||
|
|
||||||
@@ -426,76 +709,40 @@ class Agent(BaseAuditModel):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return "err"
|
return "err"
|
||||||
|
|
||||||
def salt_api_cmd(self, **kwargs):
|
async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True):
|
||||||
|
nc = NATS()
|
||||||
# salt should always timeout first before the requests' timeout
|
options = {
|
||||||
|
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||||
|
"user": "tacticalrmm",
|
||||||
|
"password": settings.SECRET_KEY,
|
||||||
|
"connect_timeout": 3,
|
||||||
|
"max_reconnect_attempts": 2,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
timeout = kwargs["timeout"]
|
await nc.connect(**options)
|
||||||
except KeyError:
|
except:
|
||||||
# default timeout
|
return "natsdown"
|
||||||
timeout = 15
|
|
||||||
salt_timeout = 12
|
if wait:
|
||||||
else:
|
try:
|
||||||
if timeout < 8:
|
msg = await nc.request(
|
||||||
timeout = 8
|
self.agent_id, msgpack.dumps(data), timeout=timeout
|
||||||
salt_timeout = 5
|
)
|
||||||
|
except ErrTimeout:
|
||||||
|
ret = "timeout"
|
||||||
else:
|
else:
|
||||||
salt_timeout = timeout - 3
|
try:
|
||||||
|
ret = msgpack.loads(msg.data) # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
ret = str(e)
|
||||||
|
|
||||||
json = {
|
await nc.close()
|
||||||
"client": "local",
|
|
||||||
"tgt": self.salt_id,
|
|
||||||
"fun": kwargs["func"],
|
|
||||||
"timeout": salt_timeout,
|
|
||||||
"username": settings.SALT_USERNAME,
|
|
||||||
"password": settings.SALT_PASSWORD,
|
|
||||||
"eauth": "pam",
|
|
||||||
}
|
|
||||||
|
|
||||||
if "arg" in kwargs:
|
|
||||||
json.update({"arg": kwargs["arg"]})
|
|
||||||
if "kwargs" in kwargs:
|
|
||||||
json.update({"kwarg": kwargs["kwargs"]})
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = requests.post(
|
|
||||||
f"http://{settings.SALT_HOST}:8123/run",
|
|
||||||
json=[json],
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
return "timeout"
|
|
||||||
|
|
||||||
try:
|
|
||||||
ret = resp.json()["return"][0][self.salt_id]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{self.salt_id}: {e}")
|
|
||||||
return "error"
|
|
||||||
else:
|
|
||||||
return ret
|
return ret
|
||||||
|
else:
|
||||||
def salt_api_async(self, **kwargs):
|
await nc.publish(self.agent_id, msgpack.dumps(data))
|
||||||
|
await nc.flush()
|
||||||
json = {
|
await nc.close()
|
||||||
"client": "local_async",
|
|
||||||
"tgt": self.salt_id,
|
|
||||||
"fun": kwargs["func"],
|
|
||||||
"username": settings.SALT_USERNAME,
|
|
||||||
"password": settings.SALT_PASSWORD,
|
|
||||||
"eauth": "pam",
|
|
||||||
}
|
|
||||||
|
|
||||||
if "arg" in kwargs:
|
|
||||||
json.update({"arg": kwargs["arg"]})
|
|
||||||
if "kwargs" in kwargs:
|
|
||||||
json.update({"kwarg": kwargs["kwargs"]})
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
|
|
||||||
except Exception:
|
|
||||||
return "timeout"
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serialize(agent):
|
def serialize(agent):
|
||||||
@@ -504,101 +751,18 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
ret = AgentEditSerializer(agent).data
|
ret = AgentEditSerializer(agent).data
|
||||||
del ret["all_timezones"]
|
del ret["all_timezones"]
|
||||||
|
del ret["client"]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def salt_batch_async(**kwargs):
|
|
||||||
assert isinstance(kwargs["minions"], list)
|
|
||||||
|
|
||||||
json = {
|
|
||||||
"client": "local_async",
|
|
||||||
"tgt_type": "list",
|
|
||||||
"tgt": kwargs["minions"],
|
|
||||||
"fun": kwargs["func"],
|
|
||||||
"username": settings.SALT_USERNAME,
|
|
||||||
"password": settings.SALT_PASSWORD,
|
|
||||||
"eauth": "pam",
|
|
||||||
}
|
|
||||||
|
|
||||||
if "arg" in kwargs:
|
|
||||||
json.update({"arg": kwargs["arg"]})
|
|
||||||
if "kwargs" in kwargs:
|
|
||||||
json.update({"kwarg": kwargs["kwargs"]})
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
|
|
||||||
except Exception:
|
|
||||||
return "timeout"
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def schedule_reboot(self, obj):
|
|
||||||
|
|
||||||
start_date = dt.datetime.strftime(obj, "%Y-%m-%d")
|
|
||||||
start_time = dt.datetime.strftime(obj, "%H:%M")
|
|
||||||
|
|
||||||
# let windows task scheduler automatically delete the task after it runs
|
|
||||||
end_obj = obj + dt.timedelta(minutes=15)
|
|
||||||
end_date = dt.datetime.strftime(end_obj, "%Y-%m-%d")
|
|
||||||
end_time = dt.datetime.strftime(end_obj, "%H:%M")
|
|
||||||
|
|
||||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
|
||||||
random.choice(string.ascii_letters) for _ in range(10)
|
|
||||||
)
|
|
||||||
|
|
||||||
r = self.salt_api_cmd(
|
|
||||||
timeout=15,
|
|
||||||
func="task.create_task",
|
|
||||||
arg=[
|
|
||||||
f"name={task_name}",
|
|
||||||
"force=True",
|
|
||||||
"action_type=Execute",
|
|
||||||
'cmd="C:\\Windows\\System32\\shutdown.exe"',
|
|
||||||
'arguments="/r /t 5 /f"',
|
|
||||||
"trigger_type=Once",
|
|
||||||
f'start_date="{start_date}"',
|
|
||||||
f'start_time="{start_time}"',
|
|
||||||
f'end_date="{end_date}"',
|
|
||||||
f'end_time="{end_time}"',
|
|
||||||
"ac_only=False",
|
|
||||||
"stop_if_on_batteries=False",
|
|
||||||
"delete_after=Immediately",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if r == "error" or (isinstance(r, bool) and not r):
|
|
||||||
return "failed"
|
|
||||||
elif r == "timeout":
|
|
||||||
return "timeout"
|
|
||||||
elif isinstance(r, bool) and r:
|
|
||||||
from logs.models import PendingAction
|
|
||||||
|
|
||||||
details = {
|
|
||||||
"taskname": task_name,
|
|
||||||
"time": str(obj),
|
|
||||||
}
|
|
||||||
PendingAction(agent=self, action_type="schedreboot", details=details).save()
|
|
||||||
|
|
||||||
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
|
||||||
return {"msg": {"time": nice_time, "agent": self.hostname}}
|
|
||||||
else:
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
def not_supported(self, version_added):
|
|
||||||
if pyver.parse(self.version) < pyver.parse(version_added):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delete_superseded_updates(self):
|
def delete_superseded_updates(self):
|
||||||
try:
|
try:
|
||||||
pks = [] # list of pks to delete
|
pks = [] # list of pks to delete
|
||||||
kbs = list(self.winupdates.values_list("kb", flat=True))
|
kbs = list(self.winupdates.values_list("kb", flat=True)) # type: ignore
|
||||||
d = Counter(kbs)
|
d = Counter(kbs)
|
||||||
dupes = [k for k, v in d.items() if v > 1]
|
dupes = [k for k, v in d.items() if v > 1]
|
||||||
|
|
||||||
for dupe in dupes:
|
for dupe in dupes:
|
||||||
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
|
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True) # type: ignore
|
||||||
# extract the version from the title and sort from oldest to newest
|
# extract the version from the title and sort from oldest to newest
|
||||||
# skip if no version info is available therefore nothing to parse
|
# skip if no version info is available therefore nothing to parse
|
||||||
try:
|
try:
|
||||||
@@ -611,69 +775,42 @@ class Agent(BaseAuditModel):
|
|||||||
continue
|
continue
|
||||||
# append all but the latest version to our list of pks to delete
|
# append all but the latest version to our list of pks to delete
|
||||||
for ver in sorted_vers[:-1]:
|
for ver in sorted_vers[:-1]:
|
||||||
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
|
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver) # type: ignore
|
||||||
pks.append(q.first().pk)
|
pks.append(q.first().pk)
|
||||||
|
|
||||||
pks = list(set(pks))
|
pks = list(set(pks))
|
||||||
self.winupdates.filter(pk__in=pks).delete()
|
self.winupdates.filter(pk__in=pks).delete() # type: ignore
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# define how the agent should handle pending actions
|
def should_create_alert(self, alert_template=None):
|
||||||
def handle_pending_actions(self):
|
return (
|
||||||
pending_actions = self.pendingactions.filter(status="pending")
|
self.overdue_dashboard_alert
|
||||||
|
or self.overdue_email_alert
|
||||||
for action in pending_actions:
|
or self.overdue_text_alert
|
||||||
if action.action_type == "taskaction":
|
or (
|
||||||
from autotasks.tasks import (
|
alert_template
|
||||||
create_win_task_schedule,
|
and (
|
||||||
enable_or_disable_win_task,
|
alert_template.agent_always_alert
|
||||||
delete_win_task_schedule,
|
or alert_template.agent_always_email
|
||||||
|
or alert_template.agent_always_text
|
||||||
)
|
)
|
||||||
|
)
|
||||||
task_id = action.details["task_id"]
|
)
|
||||||
|
|
||||||
if action.details["action"] == "taskcreate":
|
|
||||||
create_win_task_schedule.delay(task_id, pending_action=action.id)
|
|
||||||
elif action.details["action"] == "tasktoggle":
|
|
||||||
enable_or_disable_win_task.delay(
|
|
||||||
task_id, action.details["value"], pending_action=action.id
|
|
||||||
)
|
|
||||||
elif action.details["action"] == "taskdelete":
|
|
||||||
delete_win_task_schedule.delay(task_id, pending_action=action.id)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentOutage(models.Model):
|
|
||||||
agent = models.ForeignKey(
|
|
||||||
Agent,
|
|
||||||
related_name="agentoutages",
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
outage_time = models.DateTimeField(auto_now_add=True)
|
|
||||||
recovery_time = models.DateTimeField(null=True, blank=True)
|
|
||||||
outage_email_sent = models.BooleanField(default=False)
|
|
||||||
outage_sms_sent = models.BooleanField(default=False)
|
|
||||||
recovery_email_sent = models.BooleanField(default=False)
|
|
||||||
recovery_sms_sent = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_active(self):
|
|
||||||
return False if self.recovery_time else True
|
|
||||||
|
|
||||||
def send_outage_email(self):
|
def send_outage_email(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
CORE.send_mail(
|
CORE.send_mail(
|
||||||
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data overdue",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||||
(
|
(
|
||||||
f"Data has not been received from client {self.agent.client}, "
|
f"Data has not been received from client {self.client.name}, "
|
||||||
f"site {self.agent.site}, "
|
f"site {self.site.name}, "
|
||||||
f"agent {self.agent.hostname} "
|
f"agent {self.hostname} "
|
||||||
"within the expected time."
|
"within the expected time."
|
||||||
),
|
),
|
||||||
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_recovery_email(self):
|
def send_recovery_email(self):
|
||||||
@@ -681,13 +818,14 @@ class AgentOutage(models.Model):
|
|||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
CORE.send_mail(
|
CORE.send_mail(
|
||||||
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data received",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||||
(
|
(
|
||||||
f"Data has been received from client {self.agent.client}, "
|
f"Data has been received from client {self.client.name}, "
|
||||||
f"site {self.agent.site}, "
|
f"site {self.site.name}, "
|
||||||
f"agent {self.agent.hostname} "
|
f"agent {self.hostname} "
|
||||||
"after an interruption in data transmission."
|
"after an interruption in data transmission."
|
||||||
),
|
),
|
||||||
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_outage_sms(self):
|
def send_outage_sms(self):
|
||||||
@@ -695,7 +833,8 @@ class AgentOutage(models.Model):
|
|||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
CORE.send_sms(
|
CORE.send_sms(
|
||||||
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data overdue"
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||||
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_recovery_sms(self):
|
def send_recovery_sms(self):
|
||||||
@@ -703,17 +842,17 @@ class AgentOutage(models.Model):
|
|||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
CORE.send_sms(
|
CORE.send_sms(
|
||||||
f"{self.agent.client}, {self.agent.site}, {self.agent.hostname} - data received"
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||||
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.agent.hostname
|
|
||||||
|
|
||||||
|
|
||||||
RECOVERY_CHOICES = [
|
RECOVERY_CHOICES = [
|
||||||
("salt", "Salt"),
|
("salt", "Salt"),
|
||||||
("mesh", "Mesh"),
|
("mesh", "Mesh"),
|
||||||
("command", "Command"),
|
("command", "Command"),
|
||||||
|
("rpc", "Nats RPC"),
|
||||||
|
("checkrunner", "Checkrunner"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -730,12 +869,6 @@ class RecoveryAction(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.agent.hostname} - {self.mode}"
|
return f"{self.agent.hostname} - {self.mode}"
|
||||||
|
|
||||||
def send(self):
|
|
||||||
ret = {"recovery": self.mode}
|
|
||||||
if self.mode == "command":
|
|
||||||
ret["cmd"] = self.command
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class Note(models.Model):
|
class Note(models.Model):
|
||||||
agent = models.ForeignKey(
|
agent = models.ForeignKey(
|
||||||
@@ -755,3 +888,38 @@ class Note(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.agent.hostname
|
return self.agent.hostname
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCustomField(models.Model):
|
||||||
|
agent = models.ForeignKey(
|
||||||
|
Agent,
|
||||||
|
related_name="custom_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
field = models.ForeignKey(
|
||||||
|
"core.CustomField",
|
||||||
|
related_name="agent_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
string_value = models.TextField(null=True, blank=True)
|
||||||
|
bool_value = models.BooleanField(blank=True, default=False)
|
||||||
|
multiple_value = ArrayField(
|
||||||
|
models.TextField(null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.field
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self.field.type == "multiple":
|
||||||
|
return self.multiple_value
|
||||||
|
elif self.field.type == "checkbox":
|
||||||
|
return self.bool_value
|
||||||
|
else:
|
||||||
|
return self.string_value
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Agent, Note
|
from clients.serializers import ClientSerializer
|
||||||
|
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
|
from .models import Agent, AgentCustomField, Note
|
||||||
|
|
||||||
|
|
||||||
class AgentSerializer(serializers.ModelSerializer):
|
class AgentSerializer(serializers.ModelSerializer):
|
||||||
# for vue
|
# for vue
|
||||||
@@ -16,9 +16,12 @@ class AgentSerializer(serializers.ModelSerializer):
|
|||||||
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()
|
||||||
|
client_name = serializers.ReadOnlyField(source="client.name")
|
||||||
|
site_name = serializers.ReadOnlyField(source="site.name")
|
||||||
|
|
||||||
def get_all_timezones(self, obj):
|
def get_all_timezones(self, obj):
|
||||||
return pytz.all_timezones
|
return pytz.all_timezones
|
||||||
@@ -30,47 +33,118 @@ 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")
|
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")
|
||||||
|
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_last_seen(self, obj):
|
def get_alert_template(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_pending_actions(self, obj):
|
||||||
|
return obj.pendingactions.filter(status="pending").count()
|
||||||
|
|
||||||
|
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",
|
||||||
"client",
|
"site_name",
|
||||||
"site",
|
"client_name",
|
||||||
"monitoring_type",
|
"monitoring_type",
|
||||||
"description",
|
"description",
|
||||||
"needs_reboot",
|
"needs_reboot",
|
||||||
"patches_pending",
|
"patches_pending",
|
||||||
|
"pending_actions",
|
||||||
"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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
@@ -88,10 +162,13 @@ 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",
|
||||||
"all_timezones",
|
"all_timezones",
|
||||||
"winupdatepolicy",
|
"winupdatepolicy",
|
||||||
|
"policy",
|
||||||
|
"custom_fields",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +184,9 @@ class WinAgentSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class AgentHostnameSerializer(serializers.ModelSerializer):
|
class AgentHostnameSerializer(serializers.ModelSerializer):
|
||||||
|
client = serializers.ReadOnlyField(source="client.name")
|
||||||
|
site = serializers.ReadOnlyField(source="site.name")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Agent
|
model = Agent
|
||||||
fields = (
|
fields = (
|
||||||
|
|||||||
@@ -1,287 +1,280 @@
|
|||||||
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 django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
from loguru import logger
|
||||||
|
from packaging import version as pyver
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
from core.models import CodeSignToken, CoreSettings
|
||||||
|
from logs.models import PendingAction
|
||||||
|
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
|
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
|
|
||||||
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) -> 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:
|
||||||
|
logger.warning(
|
||||||
|
f"Unable to determine arch on {agent.hostname}. 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 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 send_agent_update_task(pks: list[int]) -> None:
|
||||||
assert isinstance(pks, list)
|
try:
|
||||||
|
codesigntoken = CodeSignToken.objects.first().token
|
||||||
q = Agent.objects.filter(pk__in=pks)
|
except:
|
||||||
agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)]
|
codesigntoken = None
|
||||||
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
r = agent.salt_api_async(
|
|
||||||
func="win_agent.do_agent_update_v2",
|
|
||||||
kwargs={
|
|
||||||
"inno": inno,
|
|
||||||
"url": url,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
sleep(10)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def auto_self_agent_update_task(test=False):
|
def auto_self_agent_update_task() -> None:
|
||||||
core = CoreSettings.objects.first()
|
core = CoreSettings.objects.first()
|
||||||
if not core.agent_auto_update:
|
if not core.agent_auto_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
codesigntoken = CodeSignToken.objects.first().token
|
||||||
|
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:
|
|
||||||
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
|
|
||||||
r = agent.salt_api_async(
|
|
||||||
func="win_agent.do_agent_update_v2",
|
|
||||||
kwargs={
|
|
||||||
"inno": inno,
|
|
||||||
"url": url,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not test:
|
|
||||||
sleep(10)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def update_salt_minion_task():
|
def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||||
q = Agent.objects.all()
|
from alerts.models import Alert
|
||||||
agents = [
|
|
||||||
i.pk
|
|
||||||
for i in q
|
|
||||||
if pyver.parse(i.version) >= pyver.parse("0.11.0")
|
|
||||||
and pyver.parse(i.salt_ver) < pyver.parse(settings.LATEST_SALT_VER)
|
|
||||||
]
|
|
||||||
|
|
||||||
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
|
alert = Alert.objects.get(pk=pk)
|
||||||
|
|
||||||
for chunk in chunks:
|
if not alert.email_sent:
|
||||||
for pk in chunk:
|
sleep(random.randint(1, 15))
|
||||||
agent = Agent.objects.get(pk=pk)
|
alert.agent.send_outage_email()
|
||||||
r = agent.salt_api_async(func="win_agent.update_salt")
|
alert.email_sent = djangotime.now()
|
||||||
sleep(20)
|
alert.save(update_fields=["email_sent"])
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def get_wmi_detail_task(pk):
|
|
||||||
agent = Agent.objects.get(pk=pk)
|
|
||||||
r = agent.salt_api_async(timeout=30, func="win_agent.local_sys_info")
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def sync_salt_modules_task(pk):
|
|
||||||
agent = Agent.objects.get(pk=pk)
|
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def batch_sync_modules_task():
|
|
||||||
# sync modules, split into chunks of 50 agents to not overload salt
|
|
||||||
agents = Agent.objects.all()
|
|
||||||
online = [i.salt_id for i in agents if i.status == "online"]
|
|
||||||
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 batch_sysinfo_task():
|
|
||||||
# update system info using WMI
|
|
||||||
agents = Agent.objects.all()
|
|
||||||
online = [
|
|
||||||
i.salt_id
|
|
||||||
for i in agents
|
|
||||||
if not i.not_supported("0.11.0") and i.status == "online"
|
|
||||||
]
|
|
||||||
chunks = (online[i : i + 30] for i in range(0, len(online), 30))
|
|
||||||
for chunk in chunks:
|
|
||||||
Agent.salt_batch_async(minions=chunk, func="win_agent.local_sys_info")
|
|
||||||
sleep(10)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def uninstall_agent_task(salt_id):
|
|
||||||
attempts = 0
|
|
||||||
error = False
|
|
||||||
|
|
||||||
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"])
|
|
||||||
|
|
||||||
|
agents = Agent.objects.only(
|
||||||
@app.task
|
"pk",
|
||||||
def agent_outages_task():
|
"last_seen",
|
||||||
agents = Agent.objects.only("pk")
|
"offline_time",
|
||||||
|
"overdue_time",
|
||||||
|
"overdue_email_alert",
|
||||||
|
"overdue_text_alert",
|
||||||
|
"overdue_dashboard_alert",
|
||||||
|
)
|
||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
if agent.status == "overdue":
|
if agent.status == "overdue":
|
||||||
outages = AgentOutage.objects.filter(agent=agent)
|
Alert.handle_alert_failure(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] = [],
|
||||||
|
):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if r == "timeout":
|
||||||
|
logger.error(f"{agent.hostname} 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
|
||||||
|
|
||||||
|
if emails:
|
||||||
|
msg["To"] = ", ".join(emails)
|
||||||
|
else:
|
||||||
|
msg["To"] = ", ".join(CORE.email_alert_recipients)
|
||||||
|
|
||||||
|
msg.set_content(body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server:
|
||||||
|
if CORE.smtp_requires_auth:
|
||||||
|
server.ehlo()
|
||||||
|
server.starttls()
|
||||||
|
server.login(CORE.smtp_host_user, CORE.smtp_host_password)
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
else:
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def monitor_agents_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("monitor", ids)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,16 @@
|
|||||||
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/<client>/", views.by_client),
|
|
||||||
path("bysite/<client>/<site>/", 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),
|
||||||
path("<int:pk>/meshcentral/", views.meshcentral),
|
path("<int:pk>/meshcentral/", views.meshcentral),
|
||||||
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
|
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
|
||||||
path("poweraction/", views.power_action),
|
|
||||||
path("uninstall/", views.uninstall),
|
path("uninstall/", views.uninstall),
|
||||||
path("editagent/", views.edit_agent),
|
path("editagent/", views.edit_agent),
|
||||||
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
|
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
|
||||||
@@ -20,16 +18,15 @@ urlpatterns = [
|
|||||||
path("updateagents/", views.update_agents),
|
path("updateagents/", views.update_agents),
|
||||||
path("<pk>/getprocs/", views.get_processes),
|
path("<pk>/getprocs/", views.get_processes),
|
||||||
path("<pk>/<pid>/killproc/", views.kill_proc),
|
path("<pk>/<pid>/killproc/", views.kill_proc),
|
||||||
path("rebootlater/", views.reboot_later),
|
path("reboot/", views.Reboot.as_view()),
|
||||||
path("installagent/", views.install_agent),
|
path("installagent/", views.install_agent),
|
||||||
path("<int:pk>/ping/", views.ping),
|
path("<int:pk>/ping/", views.ping),
|
||||||
path("recover/", views.recover),
|
path("recover/", views.recover),
|
||||||
path("runscript/", views.run_script),
|
path("runscript/", views.run_script),
|
||||||
path("<int:pk>/restartmesh/", views.restart_mesh),
|
|
||||||
path("<int:pk>/recovermesh/", views.recover_mesh),
|
path("<int:pk>/recovermesh/", views.recover_mesh),
|
||||||
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()),
|
||||||
]
|
]
|
||||||
|
|||||||
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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
from django.db import models
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields import BooleanField, PositiveIntegerField
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
|
|
||||||
SEVERITY_CHOICES = [
|
SEVERITY_CHOICES = [
|
||||||
("info", "Informational"),
|
("info", "Informational"),
|
||||||
@@ -7,6 +23,13 @@ SEVERITY_CHOICES = [
|
|||||||
("error", "Error"),
|
("error", "Error"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ALERT_TYPE_CHOICES = [
|
||||||
|
("availability", "Availability"),
|
||||||
|
("check", "Check"),
|
||||||
|
("task", "Task"),
|
||||||
|
("custom", "Custom"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Alert(models.Model):
|
class Alert(models.Model):
|
||||||
agent = models.ForeignKey(
|
agent = models.ForeignKey(
|
||||||
@@ -23,21 +46,584 @@ class Alert(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
assigned_task = models.ForeignKey(
|
||||||
|
"autotasks.AutomatedTask",
|
||||||
|
related_name="alert",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
alert_type = models.CharField(
|
||||||
|
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
|
||||||
|
)
|
||||||
message = models.TextField(null=True, blank=True)
|
message = models.TextField(null=True, blank=True)
|
||||||
alert_time = models.DateTimeField(auto_now_add=True, null=True)
|
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||||
|
snoozed = models.BooleanField(default=False)
|
||||||
snooze_until = models.DateTimeField(null=True, blank=True)
|
snooze_until = models.DateTimeField(null=True, blank=True)
|
||||||
resolved = models.BooleanField(default=False)
|
resolved = models.BooleanField(default=False)
|
||||||
severity = models.CharField(
|
resolved_on = models.DateTimeField(null=True, blank=True)
|
||||||
max_length=100, choices=SEVERITY_CHOICES, default="info"
|
severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
|
||||||
|
email_sent = models.DateTimeField(null=True, blank=True)
|
||||||
|
resolved_email_sent = models.DateTimeField(null=True, blank=True)
|
||||||
|
sms_sent = models.DateTimeField(null=True, blank=True)
|
||||||
|
resolved_sms_sent = models.DateTimeField(null=True, blank=True)
|
||||||
|
hidden = models.BooleanField(default=False)
|
||||||
|
action_run = models.DateTimeField(null=True, blank=True)
|
||||||
|
action_stdout = models.TextField(null=True, blank=True)
|
||||||
|
action_stderr = models.TextField(null=True, blank=True)
|
||||||
|
action_retcode = models.IntegerField(null=True, blank=True)
|
||||||
|
action_execution_time = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
resolved_action_run = models.DateTimeField(null=True, blank=True)
|
||||||
|
resolved_action_stdout = models.TextField(null=True, blank=True)
|
||||||
|
resolved_action_stderr = models.TextField(null=True, blank=True)
|
||||||
|
resolved_action_retcode = models.IntegerField(null=True, blank=True)
|
||||||
|
resolved_action_execution_time = models.CharField(
|
||||||
|
max_length=100, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
|
def resolve(self):
|
||||||
|
self.resolved = True
|
||||||
|
self.resolved_on = djangotime.now()
|
||||||
|
self.snoozed = False
|
||||||
|
self.snooze_until = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_availability_alert(cls, agent):
|
def create_or_return_availability_alert(cls, agent):
|
||||||
pass
|
if not cls.objects.filter(agent=agent, resolved=False).exists():
|
||||||
|
return cls.objects.create(
|
||||||
|
agent=agent,
|
||||||
|
alert_type="availability",
|
||||||
|
severity="error",
|
||||||
|
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
|
||||||
|
hidden=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(agent=agent, resolved=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_check_alert(cls, check):
|
def create_or_return_check_alert(cls, check):
|
||||||
pass
|
|
||||||
|
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
|
||||||
|
return cls.objects.create(
|
||||||
|
assigned_check=check,
|
||||||
|
alert_type="check",
|
||||||
|
severity=check.alert_severity,
|
||||||
|
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
|
||||||
|
hidden=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(assigned_check=check, resolved=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_or_return_task_alert(cls, task):
|
||||||
|
|
||||||
|
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
|
||||||
|
return cls.objects.create(
|
||||||
|
assigned_task=task,
|
||||||
|
alert_type="task",
|
||||||
|
severity=task.alert_severity,
|
||||||
|
message=f"{task.agent.hostname} has task: {task.name} that failed.",
|
||||||
|
hidden=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(assigned_task=task, resolved=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
# set variables
|
||||||
|
dashboard_severities = None
|
||||||
|
email_severities = None
|
||||||
|
text_severities = None
|
||||||
|
always_dashboard = None
|
||||||
|
always_email = None
|
||||||
|
always_text = None
|
||||||
|
alert_interval = None
|
||||||
|
email_task = None
|
||||||
|
text_task = None
|
||||||
|
|
||||||
|
# check what the instance passed is
|
||||||
|
if isinstance(instance, Agent):
|
||||||
|
from agents.tasks import agent_outage_email_task, agent_outage_sms_task
|
||||||
|
|
||||||
|
email_task = agent_outage_email_task
|
||||||
|
text_task = agent_outage_sms_task
|
||||||
|
|
||||||
|
email_alert = instance.overdue_email_alert
|
||||||
|
text_alert = instance.overdue_text_alert
|
||||||
|
dashboard_alert = instance.overdue_dashboard_alert
|
||||||
|
alert_template = instance.alert_template
|
||||||
|
maintenance_mode = instance.maintenance_mode
|
||||||
|
alert_severity = "error"
|
||||||
|
agent = instance
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = ["error"]
|
||||||
|
email_severities = ["error"]
|
||||||
|
text_severities = ["error"]
|
||||||
|
always_dashboard = alert_template.agent_always_alert
|
||||||
|
always_email = alert_template.agent_always_email
|
||||||
|
always_text = alert_template.agent_always_text
|
||||||
|
alert_interval = alert_template.agent_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_availability_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(agent=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(agent=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
|
||||||
|
elif isinstance(instance, Check):
|
||||||
|
from checks.tasks import (
|
||||||
|
handle_check_email_alert_task,
|
||||||
|
handle_check_sms_alert_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_task = handle_check_email_alert_task
|
||||||
|
text_task = handle_check_sms_alert_task
|
||||||
|
|
||||||
|
email_alert = instance.email_alert
|
||||||
|
text_alert = instance.text_alert
|
||||||
|
dashboard_alert = instance.dashboard_alert
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
alert_severity = instance.alert_severity
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = alert_template.check_dashboard_alert_severity
|
||||||
|
email_severities = alert_template.check_email_alert_severity
|
||||||
|
text_severities = alert_template.check_text_alert_severity
|
||||||
|
always_dashboard = alert_template.check_always_alert
|
||||||
|
always_email = alert_template.check_always_email
|
||||||
|
always_text = alert_template.check_always_text
|
||||||
|
alert_interval = alert_template.check_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_check_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(assigned_check=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(assigned_check=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
|
||||||
|
elif isinstance(instance, AutomatedTask):
|
||||||
|
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
|
||||||
|
|
||||||
|
email_task = handle_task_email_alert
|
||||||
|
text_task = handle_task_sms_alert
|
||||||
|
|
||||||
|
email_alert = instance.email_alert
|
||||||
|
text_alert = instance.text_alert
|
||||||
|
dashboard_alert = instance.dashboard_alert
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
alert_severity = instance.alert_severity
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = alert_template.task_dashboard_alert_severity
|
||||||
|
email_severities = alert_template.task_email_alert_severity
|
||||||
|
text_severities = alert_template.task_text_alert_severity
|
||||||
|
always_dashboard = alert_template.task_always_alert
|
||||||
|
always_email = alert_template.task_always_email
|
||||||
|
always_text = alert_template.task_always_text
|
||||||
|
alert_interval = alert_template.task_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_task_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(assigned_task=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(assigned_task=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if agent is in maintenance mode
|
||||||
|
if maintenance_mode or not alert:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check if alert severity changed on check and update the alert
|
||||||
|
if alert_severity != alert.severity:
|
||||||
|
alert.severity = alert_severity
|
||||||
|
alert.save(update_fields=["severity"])
|
||||||
|
|
||||||
|
# create alert in dashboard if enabled
|
||||||
|
if dashboard_alert or always_dashboard:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in dashboard_severities: # type: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
alert.hidden = False
|
||||||
|
alert.save()
|
||||||
|
|
||||||
|
# send email if enabled
|
||||||
|
if email_alert or always_email:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in email_severities: # type: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
email_task.delay(
|
||||||
|
pk=alert.pk,
|
||||||
|
alert_interval=alert_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
# send text if enabled
|
||||||
|
if text_alert or always_text:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in text_severities: # type: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
|
||||||
|
|
||||||
|
# check if any scripts should be run
|
||||||
|
if alert_template and alert_template.action and not alert.action_run:
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=alert_template.action.pk,
|
||||||
|
args=alert.parse_script_args(alert_template.action_args),
|
||||||
|
timeout=alert_template.action_timeout,
|
||||||
|
wait=True,
|
||||||
|
full=True,
|
||||||
|
run_on_any=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# command was successful
|
||||||
|
if type(r) == dict:
|
||||||
|
alert.action_retcode = r["retcode"]
|
||||||
|
alert.action_stdout = r["stdout"]
|
||||||
|
alert.action_stderr = r["stderr"]
|
||||||
|
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
|
||||||
|
alert.action_run = djangotime.now()
|
||||||
|
alert.save()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
# set variables
|
||||||
|
email_on_resolved = False
|
||||||
|
text_on_resolved = False
|
||||||
|
resolved_email_task = None
|
||||||
|
resolved_text_task = None
|
||||||
|
|
||||||
|
# check what the instance passed is
|
||||||
|
if isinstance(instance, Agent):
|
||||||
|
from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
|
||||||
|
|
||||||
|
resolved_email_task = agent_recovery_email_task
|
||||||
|
resolved_text_task = agent_recovery_sms_task
|
||||||
|
|
||||||
|
alert_template = instance.alert_template
|
||||||
|
alert = cls.objects.get(agent=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.maintenance_mode
|
||||||
|
agent = instance
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.agent_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.agent_text_on_resolved
|
||||||
|
|
||||||
|
elif isinstance(instance, Check):
|
||||||
|
from checks.tasks import (
|
||||||
|
handle_resolved_check_email_alert_task,
|
||||||
|
handle_resolved_check_sms_alert_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_email_task = handle_resolved_check_email_alert_task
|
||||||
|
resolved_text_task = handle_resolved_check_sms_alert_task
|
||||||
|
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
alert = cls.objects.get(assigned_check=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.check_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.check_text_on_resolved
|
||||||
|
|
||||||
|
elif isinstance(instance, AutomatedTask):
|
||||||
|
from autotasks.tasks import (
|
||||||
|
handle_resolved_task_email_alert,
|
||||||
|
handle_resolved_task_sms_alert,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_email_task = handle_resolved_task_email_alert
|
||||||
|
resolved_text_task = handle_resolved_task_sms_alert
|
||||||
|
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
alert = cls.objects.get(assigned_task=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.task_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.task_text_on_resolved
|
||||||
|
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if agent is in maintenance mode
|
||||||
|
if maintenance_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
alert.resolve()
|
||||||
|
|
||||||
|
# check if a resolved email notification should be send
|
||||||
|
if email_on_resolved and not alert.resolved_email_sent:
|
||||||
|
resolved_email_task.delay(pk=alert.pk)
|
||||||
|
|
||||||
|
# check if resolved text should be sent
|
||||||
|
if text_on_resolved and not alert.resolved_sms_sent:
|
||||||
|
resolved_text_task.delay(pk=alert.pk)
|
||||||
|
|
||||||
|
# check if resolved script should be run
|
||||||
|
if (
|
||||||
|
alert_template
|
||||||
|
and alert_template.resolved_action
|
||||||
|
and not alert.resolved_action_run
|
||||||
|
):
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=alert_template.resolved_action.pk,
|
||||||
|
args=alert.parse_script_args(alert_template.resolved_action_args),
|
||||||
|
timeout=alert_template.resolved_action_timeout,
|
||||||
|
wait=True,
|
||||||
|
full=True,
|
||||||
|
run_on_any=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# command was successful
|
||||||
|
if type(r) == dict:
|
||||||
|
alert.resolved_action_retcode = r["retcode"]
|
||||||
|
alert.resolved_action_stdout = r["stdout"]
|
||||||
|
alert.resolved_action_stderr = r["stderr"]
|
||||||
|
alert.resolved_action_execution_time = "{:.4f}".format(
|
||||||
|
r["execution_time"]
|
||||||
|
)
|
||||||
|
alert.resolved_action_run = djangotime.now()
|
||||||
|
alert.save()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_script_args(self, args: list[str]):
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
return []
|
||||||
|
|
||||||
|
temp_args = list()
|
||||||
|
# pattern to match for injection
|
||||||
|
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
match = pattern.match(arg)
|
||||||
|
if match:
|
||||||
|
name = match.group(1)
|
||||||
|
|
||||||
|
if hasattr(self, name):
|
||||||
|
value = getattr(self, name)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
temp_args.append(arg)
|
||||||
|
|
||||||
|
return temp_args
|
||||||
|
|
||||||
|
|
||||||
|
class AlertTemplate(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
action = models.ForeignKey(
|
||||||
|
"scripts.Script",
|
||||||
|
related_name="alert_template",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
action_args = ArrayField(
|
||||||
|
models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
action_timeout = models.PositiveIntegerField(default=15)
|
||||||
|
resolved_action = models.ForeignKey(
|
||||||
|
"scripts.Script",
|
||||||
|
related_name="resolved_alert_template",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
resolved_action_args = ArrayField(
|
||||||
|
models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
resolved_action_timeout = models.PositiveIntegerField(default=15)
|
||||||
|
|
||||||
|
# overrides the global recipients
|
||||||
|
email_recipients = ArrayField(
|
||||||
|
models.CharField(max_length=100, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
text_recipients = ArrayField(
|
||||||
|
models.CharField(max_length=100, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# overrides the from address
|
||||||
|
email_from = models.EmailField(blank=True, null=True)
|
||||||
|
|
||||||
|
# agent alert settings
|
||||||
|
agent_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
agent_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
agent_always_email = BooleanField(null=True, blank=True, default=None)
|
||||||
|
agent_always_text = BooleanField(null=True, blank=True, default=None)
|
||||||
|
agent_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||||
|
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||||
|
|
||||||
|
# check alert settings
|
||||||
|
check_email_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
check_text_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
check_dashboard_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
check_always_email = BooleanField(null=True, blank=True, default=None)
|
||||||
|
check_always_text = BooleanField(null=True, blank=True, default=None)
|
||||||
|
check_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||||
|
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||||
|
|
||||||
|
# task alert settings
|
||||||
|
task_email_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
task_text_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
task_dashboard_alert_severity = ArrayField(
|
||||||
|
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||||
|
task_always_email = BooleanField(null=True, blank=True, default=None)
|
||||||
|
task_always_text = BooleanField(null=True, blank=True, default=None)
|
||||||
|
task_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||||
|
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||||
|
|
||||||
|
# exclusion settings
|
||||||
|
exclude_workstations = BooleanField(null=True, blank=True, default=False)
|
||||||
|
exclude_servers = BooleanField(null=True, blank=True, default=False)
|
||||||
|
|
||||||
|
excluded_sites = models.ManyToManyField(
|
||||||
|
"clients.Site", related_name="alert_exclusions", blank=True
|
||||||
|
)
|
||||||
|
excluded_clients = models.ManyToManyField(
|
||||||
|
"clients.Client", related_name="alert_exclusions", blank=True
|
||||||
|
)
|
||||||
|
excluded_agents = models.ManyToManyField(
|
||||||
|
"agents.Agent", related_name="alert_exclusions", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_agent_settings(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.agent_email_on_resolved
|
||||||
|
or self.agent_text_on_resolved
|
||||||
|
or self.agent_always_email
|
||||||
|
or self.agent_always_text
|
||||||
|
or self.agent_always_alert
|
||||||
|
or bool(self.agent_periodic_alert_days)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_check_settings(self) -> bool:
|
||||||
|
return (
|
||||||
|
bool(self.check_email_alert_severity)
|
||||||
|
or bool(self.check_text_alert_severity)
|
||||||
|
or bool(self.check_dashboard_alert_severity)
|
||||||
|
or self.check_email_on_resolved
|
||||||
|
or self.check_text_on_resolved
|
||||||
|
or self.check_always_email
|
||||||
|
or self.check_always_text
|
||||||
|
or self.check_always_alert
|
||||||
|
or bool(self.check_periodic_alert_days)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_task_settings(self) -> bool:
|
||||||
|
return (
|
||||||
|
bool(self.task_email_alert_severity)
|
||||||
|
or bool(self.task_text_alert_severity)
|
||||||
|
or bool(self.task_dashboard_alert_severity)
|
||||||
|
or self.task_email_on_resolved
|
||||||
|
or self.task_text_on_resolved
|
||||||
|
or self.task_always_email
|
||||||
|
or self.task_always_text
|
||||||
|
or self.task_always_alert
|
||||||
|
or bool(self.task_periodic_alert_days)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_core_settings(self) -> bool:
|
||||||
|
return bool(self.email_from) or self.email_recipients or self.text_recipients
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_default_template(self) -> bool:
|
||||||
|
return self.default_alert_template.exists() # type: ignore
|
||||||
|
|||||||
@@ -1,19 +1,121 @@
|
|||||||
from rest_framework.serializers import (
|
from rest_framework.fields import SerializerMethodField
|
||||||
ModelSerializer,
|
from rest_framework.serializers import ModelSerializer, ReadOnlyField
|
||||||
ReadOnlyField,
|
|
||||||
DateTimeField,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import Alert
|
from automation.serializers import PolicySerializer
|
||||||
|
from clients.serializers import ClientSerializer, SiteSerializer
|
||||||
|
from tacticalrmm.utils import get_default_timezone
|
||||||
|
|
||||||
|
from .models import Alert, AlertTemplate
|
||||||
|
|
||||||
|
|
||||||
class AlertSerializer(ModelSerializer):
|
class AlertSerializer(ModelSerializer):
|
||||||
|
|
||||||
hostname = ReadOnlyField(source="agent.hostname")
|
hostname = SerializerMethodField(read_only=True)
|
||||||
client = ReadOnlyField(source="agent.client")
|
client = SerializerMethodField(read_only=True)
|
||||||
site = ReadOnlyField(source="agent.site")
|
site = SerializerMethodField(read_only=True)
|
||||||
alert_time = DateTimeField(format="iso-8601")
|
alert_time = SerializerMethodField(read_only=True)
|
||||||
|
resolve_on = SerializerMethodField(read_only=True)
|
||||||
|
snoozed_until = SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
def get_hostname(self, instance):
|
||||||
|
if instance.alert_type == "availability":
|
||||||
|
return instance.agent.hostname if instance.agent else ""
|
||||||
|
elif instance.alert_type == "check":
|
||||||
|
return (
|
||||||
|
instance.assigned_check.agent.hostname
|
||||||
|
if instance.assigned_check
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
elif instance.alert_type == "task":
|
||||||
|
return (
|
||||||
|
instance.assigned_task.agent.hostname if instance.assigned_task else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_client(self, instance):
|
||||||
|
if instance.alert_type == "availability":
|
||||||
|
return instance.agent.client.name if instance.agent else ""
|
||||||
|
elif instance.alert_type == "check":
|
||||||
|
return (
|
||||||
|
instance.assigned_check.agent.client.name
|
||||||
|
if instance.assigned_check
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
elif instance.alert_type == "task":
|
||||||
|
return (
|
||||||
|
instance.assigned_task.agent.client.name
|
||||||
|
if instance.assigned_task
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_site(self, instance):
|
||||||
|
if instance.alert_type == "availability":
|
||||||
|
return instance.agent.site.name if instance.agent else ""
|
||||||
|
elif instance.alert_type == "check":
|
||||||
|
return (
|
||||||
|
instance.assigned_check.agent.site.name
|
||||||
|
if instance.assigned_check
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
elif instance.alert_type == "task":
|
||||||
|
return (
|
||||||
|
instance.assigned_task.agent.site.name if instance.assigned_task else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_alert_time(self, instance):
|
||||||
|
if instance.alert_time:
|
||||||
|
return instance.alert_time.astimezone(get_default_timezone()).timestamp()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_resolve_on(self, instance):
|
||||||
|
if instance.resolved_on:
|
||||||
|
return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_snoozed_until(self, instance):
|
||||||
|
if instance.snooze_until:
|
||||||
|
return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
|
||||||
|
return None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Alert
|
model = Alert
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AlertTemplateSerializer(ModelSerializer):
|
||||||
|
agent_settings = ReadOnlyField(source="has_agent_settings")
|
||||||
|
check_settings = ReadOnlyField(source="has_check_settings")
|
||||||
|
task_settings = ReadOnlyField(source="has_task_settings")
|
||||||
|
core_settings = ReadOnlyField(source="has_core_settings")
|
||||||
|
default_template = ReadOnlyField(source="is_default_template")
|
||||||
|
action_name = ReadOnlyField(source="action.name")
|
||||||
|
resolved_action_name = ReadOnlyField(source="resolved_action.name")
|
||||||
|
applied_count = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AlertTemplate
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def get_applied_count(self, instance):
|
||||||
|
count = 0
|
||||||
|
count += instance.policies.count()
|
||||||
|
count += instance.clients.count()
|
||||||
|
count += instance.sites.count()
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
class AlertTemplateRelationSerializer(ModelSerializer):
|
||||||
|
policies = PolicySerializer(read_only=True, many=True)
|
||||||
|
clients = ClientSerializer(read_only=True, many=True)
|
||||||
|
sites = SiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AlertTemplate
|
||||||
|
fields = "__all__"
|
||||||
|
|||||||
24
api/tacticalrmm/alerts/tasks.py
Normal file
24
api/tacticalrmm/alerts/tasks.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.utils import timezone as djangotime
|
||||||
|
|
||||||
|
from alerts.models import Alert
|
||||||
|
from tacticalrmm.celery import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def unsnooze_alerts() -> str:
|
||||||
|
|
||||||
|
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
|
||||||
|
snoozed=False, snooze_until=None
|
||||||
|
)
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def cache_agents_alert_template():
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
for agent in Agent.objects.only("pk"):
|
||||||
|
agent.set_alert_template()
|
||||||
|
|
||||||
|
return "ok"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("alerts/", views.GetAddAlerts.as_view()),
|
path("alerts/", views.GetAddAlerts.as_view()),
|
||||||
|
path("bulk/", views.BulkAlerts.as_view()),
|
||||||
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
|
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
|
||||||
|
path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
|
||||||
|
path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
|
||||||
|
path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,19 +1,104 @@
|
|||||||
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from .models import Alert
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .serializers import AlertSerializer
|
from .models import Alert, AlertTemplate
|
||||||
|
from .serializers import (
|
||||||
|
AlertSerializer,
|
||||||
|
AlertTemplateRelationSerializer,
|
||||||
|
AlertTemplateSerializer,
|
||||||
|
)
|
||||||
|
from .tasks import cache_agents_alert_template
|
||||||
|
|
||||||
|
|
||||||
class GetAddAlerts(APIView):
|
class GetAddAlerts(APIView):
|
||||||
def get(self, request):
|
def patch(self, request):
|
||||||
alerts = Alert.objects.all()
|
|
||||||
|
|
||||||
return Response(AlertSerializer(alerts, many=True).data)
|
# top 10 alerts for dashboard icon
|
||||||
|
if "top" in request.data.keys():
|
||||||
|
alerts = Alert.objects.filter(
|
||||||
|
resolved=False, snoozed=False, hidden=False
|
||||||
|
).order_by("alert_time")[: int(request.data["top"])]
|
||||||
|
count = Alert.objects.filter(
|
||||||
|
resolved=False, snoozed=False, hidden=False
|
||||||
|
).count()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"alerts_count": count,
|
||||||
|
"alerts": AlertSerializer(alerts, many=True).data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif any(
|
||||||
|
key
|
||||||
|
in [
|
||||||
|
"timeFilter",
|
||||||
|
"clientFilter",
|
||||||
|
"severityFilter",
|
||||||
|
"resolvedFilter",
|
||||||
|
"snoozedFilter",
|
||||||
|
]
|
||||||
|
for key in request.data.keys()
|
||||||
|
):
|
||||||
|
clientFilter = Q()
|
||||||
|
severityFilter = Q()
|
||||||
|
timeFilter = Q()
|
||||||
|
resolvedFilter = Q()
|
||||||
|
snoozedFilter = Q()
|
||||||
|
|
||||||
|
if (
|
||||||
|
"snoozedFilter" in request.data.keys()
|
||||||
|
and not request.data["snoozedFilter"]
|
||||||
|
):
|
||||||
|
snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
|
||||||
|
|
||||||
|
if (
|
||||||
|
"resolvedFilter" in request.data.keys()
|
||||||
|
and not request.data["resolvedFilter"]
|
||||||
|
):
|
||||||
|
resolvedFilter = Q(resolved=request.data["resolvedFilter"])
|
||||||
|
|
||||||
|
if "clientFilter" in request.data.keys():
|
||||||
|
from agents.models import Agent
|
||||||
|
from clients.models import Client
|
||||||
|
|
||||||
|
clients = Client.objects.filter(
|
||||||
|
pk__in=request.data["clientFilter"]
|
||||||
|
).values_list("id")
|
||||||
|
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
|
||||||
|
"id"
|
||||||
|
)
|
||||||
|
|
||||||
|
clientFilter = Q(agent__in=agents)
|
||||||
|
|
||||||
|
if "severityFilter" in request.data.keys():
|
||||||
|
severityFilter = Q(severity__in=request.data["severityFilter"])
|
||||||
|
|
||||||
|
if "timeFilter" in request.data.keys():
|
||||||
|
timeFilter = Q(
|
||||||
|
alert_time__lte=djangotime.make_aware(dt.today()),
|
||||||
|
alert_time__gt=djangotime.make_aware(dt.today())
|
||||||
|
- djangotime.timedelta(days=int(request.data["timeFilter"])),
|
||||||
|
)
|
||||||
|
|
||||||
|
alerts = (
|
||||||
|
Alert.objects.filter(clientFilter)
|
||||||
|
.filter(severityFilter)
|
||||||
|
.filter(resolvedFilter)
|
||||||
|
.filter(snoozedFilter)
|
||||||
|
.filter(timeFilter)
|
||||||
|
)
|
||||||
|
return Response(AlertSerializer(alerts, many=True).data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
alerts = Alert.objects.all()
|
||||||
|
return Response(AlertSerializer(alerts, many=True).data)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = AlertSerializer(data=request.data, partial=True)
|
serializer = AlertSerializer(data=request.data, partial=True)
|
||||||
@@ -32,7 +117,40 @@ class GetUpdateDeleteAlert(APIView):
|
|||||||
def put(self, request, pk):
|
def put(self, request, pk):
|
||||||
alert = get_object_or_404(Alert, pk=pk)
|
alert = get_object_or_404(Alert, pk=pk)
|
||||||
|
|
||||||
serializer = AlertSerializer(instance=alert, data=request.data, partial=True)
|
data = request.data
|
||||||
|
|
||||||
|
if "type" in data.keys():
|
||||||
|
if data["type"] == "resolve":
|
||||||
|
data = {
|
||||||
|
"resolved": True,
|
||||||
|
"resolved_on": djangotime.now(),
|
||||||
|
"snoozed": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# unable to set snooze_until to none in serialzier
|
||||||
|
alert.snooze_until = None
|
||||||
|
alert.save()
|
||||||
|
elif data["type"] == "snooze":
|
||||||
|
if "snooze_days" in data.keys():
|
||||||
|
data = {
|
||||||
|
"snoozed": True,
|
||||||
|
"snooze_until": djangotime.now()
|
||||||
|
+ djangotime.timedelta(days=int(data["snooze_days"])),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return notify_error(
|
||||||
|
"Missing 'snoozed_days' when trying to snooze alert"
|
||||||
|
)
|
||||||
|
elif data["type"] == "unsnooze":
|
||||||
|
data = {"snoozed": False}
|
||||||
|
|
||||||
|
# unable to set snooze_until to none in serialzier
|
||||||
|
alert.snooze_until = None
|
||||||
|
alert.save()
|
||||||
|
else:
|
||||||
|
return notify_error("There was an error in the request data")
|
||||||
|
|
||||||
|
serializer = AlertSerializer(instance=alert, data=data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
@@ -42,3 +160,77 @@ class GetUpdateDeleteAlert(APIView):
|
|||||||
Alert.objects.get(pk=pk).delete()
|
Alert.objects.get(pk=pk).delete()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class BulkAlerts(APIView):
|
||||||
|
def post(self, request):
|
||||||
|
if request.data["bulk_action"] == "resolve":
|
||||||
|
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||||
|
resolved=True,
|
||||||
|
resolved_on=djangotime.now(),
|
||||||
|
snoozed=False,
|
||||||
|
snooze_until=None,
|
||||||
|
)
|
||||||
|
return Response("ok")
|
||||||
|
elif request.data["bulk_action"] == "snooze":
|
||||||
|
if "snooze_days" in request.data.keys():
|
||||||
|
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||||
|
snoozed=True,
|
||||||
|
snooze_until=djangotime.now()
|
||||||
|
+ djangotime.timedelta(days=int(request.data["snooze_days"])),
|
||||||
|
)
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
return notify_error("The request was invalid")
|
||||||
|
|
||||||
|
|
||||||
|
class GetAddAlertTemplates(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
alert_templates = AlertTemplate.objects.all()
|
||||||
|
|
||||||
|
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = AlertTemplateSerializer(data=request.data, partial=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class GetUpdateDeleteAlertTemplate(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||||
|
|
||||||
|
return Response(AlertTemplateSerializer(alert_template).data)
|
||||||
|
|
||||||
|
def put(self, request, pk):
|
||||||
|
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||||
|
|
||||||
|
serializer = AlertTemplateSerializer(
|
||||||
|
instance=alert_template, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
get_object_or_404(AlertTemplate, pk=pk).delete()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedAlertTemplate(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||||
|
return Response(AlertTemplateRelationSerializer(alert_template).data)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
|
||||||
name = "api"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
from apiv3 import views as v3_views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("triggerpatchscan/", views.trigger_patch_scan),
|
|
||||||
path("<int:pk>/checkrunner/", views.CheckRunner.as_view()),
|
|
||||||
path("<int:pk>/taskrunner/", views.TaskRunner.as_view()),
|
|
||||||
path("<int:pk>/saltinfo/", views.SaltInfo.as_view()),
|
|
||||||
path("<int:pk>/meshinfo/", v3_views.MeshInfo.as_view()),
|
|
||||||
]
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
from loguru import logger
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.utils import timezone as djangotime
|
|
||||||
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.authentication import TokenAuthentication
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.decorators import (
|
|
||||||
api_view,
|
|
||||||
authentication_classes,
|
|
||||||
permission_classes,
|
|
||||||
)
|
|
||||||
|
|
||||||
from agents.models import Agent
|
|
||||||
from checks.models import Check
|
|
||||||
from autotasks.models import AutomatedTask
|
|
||||||
|
|
||||||
from winupdate.tasks import check_for_updates_task
|
|
||||||
|
|
||||||
from autotasks.serializers import TaskRunnerGetSerializer, TaskRunnerPatchSerializer
|
|
||||||
from checks.serializers import CheckRunnerGetSerializer, CheckResultsSerializer
|
|
||||||
|
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["PATCH"])
|
|
||||||
@authentication_classes((TokenAuthentication,))
|
|
||||||
@permission_classes((IsAuthenticated,))
|
|
||||||
def trigger_patch_scan(request):
|
|
||||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
|
||||||
reboot_policy = agent.get_patch_policy().reboot_after_install
|
|
||||||
reboot = False
|
|
||||||
|
|
||||||
if reboot_policy == "always":
|
|
||||||
reboot = True
|
|
||||||
|
|
||||||
if request.data["reboot"]:
|
|
||||||
if reboot_policy == "required":
|
|
||||||
reboot = True
|
|
||||||
elif reboot_policy == "never":
|
|
||||||
agent.needs_reboot = True
|
|
||||||
agent.save(update_fields=["needs_reboot"])
|
|
||||||
|
|
||||||
if reboot:
|
|
||||||
r = agent.salt_api_cmd(
|
|
||||||
timeout=15,
|
|
||||||
func="system.reboot",
|
|
||||||
arg=7,
|
|
||||||
kwargs={"in_seconds": True},
|
|
||||||
)
|
|
||||||
|
|
||||||
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
|
|
||||||
check_for_updates_task.apply_async(
|
|
||||||
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
|
|
||||||
else:
|
|
||||||
check_for_updates_task.apply_async(
|
|
||||||
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response("ok")
|
|
||||||
|
|
||||||
|
|
||||||
class CheckRunner(APIView):
|
|
||||||
"""
|
|
||||||
For windows agent
|
|
||||||
"""
|
|
||||||
|
|
||||||
authentication_classes = [TokenAuthentication]
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
|
||||||
checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False)
|
|
||||||
|
|
||||||
ret = {
|
|
||||||
"agent": agent.pk,
|
|
||||||
"check_interval": agent.check_interval,
|
|
||||||
"checks": CheckRunnerGetSerializer(checks, many=True).data,
|
|
||||||
}
|
|
||||||
return Response(ret)
|
|
||||||
|
|
||||||
def patch(self, request, pk):
|
|
||||||
check = get_object_or_404(Check, pk=pk)
|
|
||||||
|
|
||||||
if check.check_type != "cpuload" and check.check_type != "memory":
|
|
||||||
serializer = CheckResultsSerializer(
|
|
||||||
instance=check, data=request.data, partial=True
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save(last_run=djangotime.now())
|
|
||||||
|
|
||||||
else:
|
|
||||||
check.last_run = djangotime.now()
|
|
||||||
check.save(update_fields=["last_run"])
|
|
||||||
|
|
||||||
check.handle_check(request.data)
|
|
||||||
|
|
||||||
return Response("ok")
|
|
||||||
|
|
||||||
|
|
||||||
class TaskRunner(APIView):
|
|
||||||
"""
|
|
||||||
For the windows python agent
|
|
||||||
"""
|
|
||||||
|
|
||||||
authentication_classes = [TokenAuthentication]
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
|
||||||
|
|
||||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
|
||||||
return Response(TaskRunnerGetSerializer(task).data)
|
|
||||||
|
|
||||||
def patch(self, request, pk):
|
|
||||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
|
||||||
|
|
||||||
serializer = TaskRunnerPatchSerializer(
|
|
||||||
instance=task, data=request.data, partial=True
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save(last_run=djangotime.now())
|
|
||||||
return Response("ok")
|
|
||||||
|
|
||||||
|
|
||||||
class SaltInfo(APIView):
|
|
||||||
authentication_classes = [TokenAuthentication]
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
|
||||||
ret = {
|
|
||||||
"latestVer": settings.LATEST_SALT_VER,
|
|
||||||
"currentVer": agent.salt_ver,
|
|
||||||
"salt_id": agent.salt_id,
|
|
||||||
}
|
|
||||||
return Response(ret)
|
|
||||||
|
|
||||||
def patch(self, request, pk):
|
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
|
||||||
agent.salt_ver = request.data["ver"]
|
|
||||||
agent.save(update_fields=["salt_ver"])
|
|
||||||
return Response("ok")
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class Apiv2Config(AppConfig):
|
|
||||||
name = 'apiv2'
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from tacticalrmm.test import TacticalTestCase
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestAPIv2(TacticalTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.authenticate()
|
|
||||||
self.setup_coresettings()
|
|
||||||
self.agent_setup()
|
|
||||||
|
|
||||||
@patch("agents.models.Agent.salt_api_cmd")
|
|
||||||
def test_sync_modules(self, mock_ret):
|
|
||||||
url = "/api/v2/saltminion/"
|
|
||||||
payload = {"agent_id": self.agent.agent_id}
|
|
||||||
|
|
||||||
mock_ret.return_value = "error"
|
|
||||||
r = self.client.patch(url, payload, format="json")
|
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
|
|
||||||
mock_ret.return_value = []
|
|
||||||
r = self.client.patch(url, payload, format="json")
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertEqual(r.data, "Modules are already in sync")
|
|
||||||
|
|
||||||
mock_ret.return_value = ["modules.win_agent"]
|
|
||||||
r = self.client.patch(url, payload, format="json")
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertEqual(r.data, "Successfully synced salt modules")
|
|
||||||
|
|
||||||
mock_ret.return_value = ["askdjaskdjasd", "modules.win_agent"]
|
|
||||||
r = self.client.patch(url, payload, format="json")
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertEqual(r.data, "Successfully synced salt modules")
|
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
from apiv3 import views as v3_views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("newagent/", v3_views.NewAgent.as_view()),
|
|
||||||
path("meshexe/", v3_views.MeshExe.as_view()),
|
|
||||||
path("saltminion/", v3_views.SaltMinion.as_view()),
|
|
||||||
path("<str:agentid>/saltminion/", v3_views.SaltMinion.as_view()),
|
|
||||||
path("sysinfo/", v3_views.SysInfo.as_view()),
|
|
||||||
path("hello/", v3_views.Hello.as_view()),
|
|
||||||
path("checkrunner/", views.CheckRunner.as_view()),
|
|
||||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
|
||||||
]
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.utils import timezone as djangotime
|
|
||||||
|
|
||||||
from rest_framework.authentication import TokenAuthentication
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from agents.models import Agent
|
|
||||||
from checks.models import Check
|
|
||||||
|
|
||||||
from checks.serializers import CheckRunnerGetSerializerV2
|
|
||||||
|
|
||||||
|
|
||||||
class CheckRunner(APIView):
|
|
||||||
"""
|
|
||||||
For the windows python agent
|
|
||||||
"""
|
|
||||||
|
|
||||||
authentication_classes = [TokenAuthentication]
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def get(self, request, agentid):
|
|
||||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
|
||||||
agent.last_seen = djangotime.now()
|
|
||||||
agent.save(update_fields=["last_seen"])
|
|
||||||
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
|
|
||||||
|
|
||||||
ret = {
|
|
||||||
"agent": agent.pk,
|
|
||||||
"check_interval": agent.check_interval,
|
|
||||||
"checks": CheckRunnerGetSerializerV2(checks, many=True).data,
|
|
||||||
}
|
|
||||||
return Response(ret)
|
|
||||||
|
|
||||||
def patch(self, request):
|
|
||||||
check = get_object_or_404(Check, pk=request.data["id"])
|
|
||||||
check.last_run = djangotime.now()
|
|
||||||
check.save(update_fields=["last_run"])
|
|
||||||
status = check.handle_checkv2(request.data)
|
|
||||||
return Response(status)
|
|
||||||
@@ -1,17 +1,62 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from django.conf import settings
|
|
||||||
from tacticalrmm.test import BaseTestCase
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
from model_bakery import baker
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
|
||||||
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPIv3(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.authenticate()
|
||||||
|
self.setup_coresettings()
|
||||||
|
self.agent = baker.make_recipe("agents.agent")
|
||||||
|
|
||||||
class TestAPIv3(BaseTestCase):
|
|
||||||
def test_get_checks(self):
|
def test_get_checks(self):
|
||||||
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
|
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
|
||||||
|
|
||||||
|
# add a check
|
||||||
|
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
|
||||||
|
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
|
||||||
|
|
||||||
|
# override check run interval
|
||||||
|
check2 = baker.make_recipe(
|
||||||
|
"checks.ping_check", agent=self.agent, run_interval=20
|
||||||
|
)
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
|
||||||
|
|
||||||
|
# Set last_run on both checks and should return an empty list
|
||||||
|
check1.last_run = djangotime.now()
|
||||||
|
check1.save()
|
||||||
|
check2.last_run = djangotime.now()
|
||||||
|
check2.save()
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertFalse(r.data["checks"]) # type: ignore
|
||||||
|
|
||||||
|
# set last_run greater than interval
|
||||||
|
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||||
|
check1.save()
|
||||||
|
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||||
|
check2.save()
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
|
||||||
|
|
||||||
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
@@ -19,46 +64,10 @@ class TestAPIv3(BaseTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_get_salt_minion(self):
|
|
||||||
url = f"/api/v3/{self.agent.agent_id}/saltminion/"
|
|
||||||
url2 = f"/api/v2/{self.agent.agent_id}/saltminion/"
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertIn("latestVer", r.json().keys())
|
|
||||||
self.assertIn("currentVer", r.json().keys())
|
|
||||||
self.assertIn("salt_id", r.json().keys())
|
|
||||||
self.assertIn("downloadURL", r.json().keys())
|
|
||||||
|
|
||||||
r2 = self.client.get(url2)
|
|
||||||
self.assertEqual(r2.status_code, 200)
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
self.check_not_authenticated("get", url2)
|
|
||||||
|
|
||||||
def test_get_mesh_info(self):
|
|
||||||
url = f"/api/v3/{self.agent.pk}/meshinfo/"
|
|
||||||
url2 = f"/api/v1/{self.agent.pk}/meshinfo/"
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
r = self.client.get(url2)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
self.check_not_authenticated("get", url2)
|
|
||||||
|
|
||||||
def test_get_winupdater(self):
|
|
||||||
url = f"/api/v3/{self.agent.agent_id}/winupdater/"
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
|
|
||||||
def test_sysinfo(self):
|
def test_sysinfo(self):
|
||||||
# TODO replace this with golang wmi sample data
|
# TODO replace this with golang wmi sample data
|
||||||
|
|
||||||
url = f"/api/v3/sysinfo/"
|
url = "/api/v3/sysinfo/"
|
||||||
with open(
|
with open(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
|
settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
|
||||||
@@ -73,19 +82,260 @@ class TestAPIv3(BaseTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
def test_hello_patch(self):
|
def test_checkrunner_interval(self):
|
||||||
url = f"/api/v3/hello/"
|
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(),
|
||||||
|
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
|
||||||
|
)
|
||||||
|
|
||||||
|
# add check to agent with check interval set
|
||||||
|
check = baker.make_recipe(
|
||||||
|
"checks.ping_check", agent=self.agent, run_interval=30
|
||||||
|
)
|
||||||
|
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(),
|
||||||
|
{"agent": self.agent.pk, "check_interval": 30},
|
||||||
|
)
|
||||||
|
|
||||||
|
# minimum check run interval is 15 seconds
|
||||||
|
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
|
||||||
|
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(),
|
||||||
|
{"agent": self.agent.pk, "check_interval": 15},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_checks(self):
|
||||||
|
# force run all checks regardless of interval
|
||||||
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
|
baker.make_recipe("checks.ping_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.cpuload_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.memory_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.eventlog_check", agent=agent)
|
||||||
|
for _ in range(10):
|
||||||
|
baker.make_recipe("checks.script_check", agent=agent)
|
||||||
|
|
||||||
|
url = f"/api/v3/{agent.agent_id}/runchecks/"
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.json()["agent"], agent.pk)
|
||||||
|
self.assertIsInstance(r.json()["check_interval"], int)
|
||||||
|
self.assertEqual(len(r.json()["checks"]), 15)
|
||||||
|
|
||||||
|
def test_checkin_patch(self):
|
||||||
|
from logs.models import PendingAction
|
||||||
|
|
||||||
|
url = "/api/v3/checkin/"
|
||||||
|
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
|
||||||
|
PendingAction.objects.create(
|
||||||
|
agent=agent_updated,
|
||||||
|
action_type="agentupdate",
|
||||||
|
details={
|
||||||
|
"url": agent_updated.winagent_dl,
|
||||||
|
"version": agent_updated.version,
|
||||||
|
"inno": agent_updated.win_inno_exe,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "pending")
|
||||||
|
|
||||||
|
# test agent failed to update and still on same version
|
||||||
payload = {
|
payload = {
|
||||||
"agent_id": self.agent.agent_id,
|
"func": "hello",
|
||||||
"logged_in_username": "None",
|
"agent_id": agent_updated.agent_id,
|
||||||
"disks": [],
|
"version": "1.3.0",
|
||||||
|
}
|
||||||
|
r = self.client.patch(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "pending")
|
||||||
|
|
||||||
|
# test agent successful update
|
||||||
|
payload["version"] = settings.LATEST_AGENT_VER
|
||||||
|
r = self.client.patch(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "completed")
|
||||||
|
action.delete()
|
||||||
|
|
||||||
|
@patch("apiv3.views.reload_nats")
|
||||||
|
def test_agent_recovery(self, reload_nats):
|
||||||
|
reload_nats.return_value = "ok"
|
||||||
|
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
|
url = f"/api/v3/{agent.agent_id}/recovery/"
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make(
|
||||||
|
"agents.RecoveryAction",
|
||||||
|
agent=agent,
|
||||||
|
mode="command",
|
||||||
|
command="shutdown /r /t 5 /f",
|
||||||
|
)
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
|
||||||
|
)
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
|
||||||
|
reload_nats.assert_called_once()
|
||||||
|
|
||||||
|
def test_task_runner_get(self):
|
||||||
|
from autotasks.serializers import TaskGOGetSerializer
|
||||||
|
|
||||||
|
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
# setup data
|
||||||
|
agent = baker.make_recipe("agents.agent")
|
||||||
|
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||||
|
|
||||||
|
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore
|
||||||
|
|
||||||
|
def test_task_runner_results(self):
|
||||||
|
from agents.models import AgentCustomField
|
||||||
|
|
||||||
|
r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
# setup data
|
||||||
|
agent = baker.make_recipe("agents.agent")
|
||||||
|
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||||
|
|
||||||
|
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
|
||||||
|
|
||||||
|
# test passing task
|
||||||
|
data = {
|
||||||
|
"stdout": "test test \ntestest stdgsd\n",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 0,
|
||||||
|
"execution_time": 3.560,
|
||||||
}
|
}
|
||||||
|
|
||||||
r = self.client.patch(url, payload, format="json")
|
r = self.client.patch(url, data)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore
|
||||||
|
|
||||||
payload["logged_in_username"] = "Bob"
|
# test failing task
|
||||||
r = self.client.patch(url, payload, format="json")
|
data = {
|
||||||
|
"stdout": "test test \ntestest stdgsd\n",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 1,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
# test collector task
|
||||||
|
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
|
||||||
|
boolean = baker.make(
|
||||||
|
"core.CustomField", model="agent", type="checkbox", name="Test1"
|
||||||
|
)
|
||||||
|
multiple = baker.make(
|
||||||
|
"core.CustomField", model="agent", type="multiple", name="Test2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# test text fields
|
||||||
|
task.custom_field = text # type: ignore
|
||||||
|
task.save() # type: ignore
|
||||||
|
|
||||||
|
# test failing failing with stderr
|
||||||
|
data = {
|
||||||
|
"stdout": "test test \nthe last line",
|
||||||
|
"stderr": "This is an error",
|
||||||
|
"retcode": 1,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
|
||||||
|
|
||||||
|
# test saving to text field
|
||||||
|
data = {
|
||||||
|
"stdout": "test test \nthe last line",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 0,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||||
|
self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore
|
||||||
|
|
||||||
|
# test saving to checkbox field
|
||||||
|
task.custom_field = boolean # type: ignore
|
||||||
|
task.save() # type: ignore
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"stdout": "1",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 0,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||||
|
self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore
|
||||||
|
|
||||||
|
# test saving to multiple field with commas
|
||||||
|
task.custom_field = multiple # type: ignore
|
||||||
|
task.save() # type: ignore
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"stdout": "this,is,an,array",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 0,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||||
|
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore
|
||||||
|
|
||||||
|
# test mutiple with a single value
|
||||||
|
data = {
|
||||||
|
"stdout": "this",
|
||||||
|
"stderr": "",
|
||||||
|
"retcode": 0,
|
||||||
|
"execution_time": 3.560,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||||
|
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user