Compare commits
1153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b63b2002a9 | ||
|
|
059edc36e4 | ||
|
|
902034ecf0 | ||
|
|
4d27f2b594 | ||
|
|
f73ea8f9f4 | ||
|
|
596ec3eb2c | ||
|
|
cb71319ff0 | ||
|
|
7b7164a9a2 | ||
|
|
721ce8f91a | ||
|
|
9fdbae986c | ||
|
|
9535a9fa3f |
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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,276 +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)
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
sleep(0.05)
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
sleep(4)
|
||||||
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():
|
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)
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
sleep(0.05)
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
sleep(4)
|
||||||
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 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