Compare commits
539 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee246440f | ||
|
|
e2f524ce7a | ||
|
|
a58b054292 | ||
|
|
ea9e5be1fc | ||
|
|
760ea4727c | ||
|
|
f57f2e53a0 | ||
|
|
136a393a17 | ||
|
|
8bbaab78b7 | ||
|
|
067cd59637 | ||
|
|
ce6ac7bf53 | ||
|
|
99271c4477 | ||
|
|
156142ed58 | ||
|
|
4b5516c0eb | ||
|
|
c3d8d2d240 | ||
|
|
c29cf70025 | ||
|
|
6ebce55be3 | ||
|
|
01c4a85bc0 | ||
|
|
12d4206d84 | ||
|
|
946de18bea | ||
|
|
904eb3538c | ||
|
|
c851ca9328 | ||
|
|
0ac415ad83 | ||
|
|
b3ba34d980 | ||
|
|
52740271d9 | ||
|
|
c2e444249a | ||
|
|
97310b091e | ||
|
|
4dda9cc3a1 | ||
|
|
a0538b57e2 | ||
|
|
d7f394eeb6 | ||
|
|
1bc4571d42 | ||
|
|
22e878502a | ||
|
|
03c1b6e30c | ||
|
|
374a434d98 | ||
|
|
f1e85ff0e9 | ||
|
|
6b010f76ea | ||
|
|
0c3e9f7824 | ||
|
|
ccca578622 | ||
|
|
56f7c18550 | ||
|
|
d438f71bbb | ||
|
|
ca5df24b6d | ||
|
|
4a6c2d106f | ||
|
|
cd25a9568b | ||
|
|
f78a787adb | ||
|
|
dc520fa77c | ||
|
|
8f06d4dd9d | ||
|
|
a7047183e1 | ||
|
|
c0b145da24 | ||
|
|
52e7fd6f72 | ||
|
|
4bbe22b1c7 | ||
|
|
4747ffc08b | ||
|
|
9d07131fd6 | ||
|
|
721126d3db | ||
|
|
2b65f5e3dc | ||
|
|
57f10cf387 | ||
|
|
f60c8a173b | ||
|
|
857cd690be | ||
|
|
a407b60152 | ||
|
|
2c3c55adc0 | ||
|
|
f586b4da17 | ||
|
|
0b7eb41049 | ||
|
|
bd19c4e2bd | ||
|
|
e8a73087d6 | ||
|
|
dde4fd82f4 | ||
|
|
0420c393f3 | ||
|
|
c88dac6437 | ||
|
|
cd450f55e2 | ||
|
|
190ee7f9fb | ||
|
|
fd057300cc | ||
|
|
56791089c1 | ||
|
|
e91cb32ca3 | ||
|
|
9ab20df8d2 | ||
|
|
050350501c | ||
|
|
d078acdf73 | ||
|
|
b786a688b5 | ||
|
|
6b7fe40dd2 | ||
|
|
6f6c422246 | ||
|
|
d371ff4f60 | ||
|
|
d1a8348912 | ||
|
|
be956d3cb6 | ||
|
|
ba5beb81b7 | ||
|
|
106bbe5244 | ||
|
|
f39d0e7ba2 | ||
|
|
de7a1fd8ff | ||
|
|
1ac2b25876 | ||
|
|
9e014d1371 | ||
|
|
93b274a113 | ||
|
|
474c7ae873 | ||
|
|
31690d4cad | ||
|
|
bbfc7e7e49 | ||
|
|
1c0aa55e7a | ||
|
|
29778ca19e | ||
|
|
9e87318cc5 | ||
|
|
c645be6b70 | ||
|
|
57fc5ac088 | ||
|
|
924774f52a | ||
|
|
446a7a0844 | ||
|
|
5cfeed76d0 | ||
|
|
de419319d8 | ||
|
|
7a3d36899b | ||
|
|
f5dbb363f4 | ||
|
|
2bbc59a212 | ||
|
|
3403d76aae | ||
|
|
58399cedb6 | ||
|
|
9bca7e9e11 | ||
|
|
3a61430e44 | ||
|
|
7d8c783a7d | ||
|
|
a2e996b550 | ||
|
|
cfc1c31050 | ||
|
|
45106bf6f9 | ||
|
|
6e3cfe491b | ||
|
|
12f2158afd | ||
|
|
6d78773c55 | ||
|
|
43a62d4eb6 | ||
|
|
cc08dfda96 | ||
|
|
622e33588e | ||
|
|
67980b58a0 | ||
|
|
027e444955 | ||
|
|
d838750389 | ||
|
|
71d8bd5266 | ||
|
|
ec4ae24bbd | ||
|
|
1128149359 | ||
|
|
bdfc6634ec | ||
|
|
ca4d19667b | ||
|
|
c71aa7baa7 | ||
|
|
fd80ccd2c5 | ||
|
|
9dc0b24399 | ||
|
|
747954e6fb | ||
|
|
274f4f227e | ||
|
|
92197d8d49 | ||
|
|
aee06920eb | ||
|
|
5111b17d3c | ||
|
|
2849d8f45d | ||
|
|
bac60d9bd4 | ||
|
|
9c797162f4 | ||
|
|
09d184e2f8 | ||
|
|
7bca618906 | ||
|
|
67607103e9 | ||
|
|
73c9956fe4 | ||
|
|
b42f2ffe33 | ||
|
|
30a3f185ef | ||
|
|
4f1b41227f | ||
|
|
83b9d13ec9 | ||
|
|
cee7896c37 | ||
|
|
0377009d2b | ||
|
|
b472f3644e | ||
|
|
5d8ea837c8 | ||
|
|
82de6bc849 | ||
|
|
cb4bc68c48 | ||
|
|
3ce6b38247 | ||
|
|
716c0fe979 | ||
|
|
c993790b7a | ||
|
|
aa32286531 | ||
|
|
6f94abde00 | ||
|
|
fa19538c9d | ||
|
|
84c858b878 | ||
|
|
865de142d4 | ||
|
|
9118162553 | ||
|
|
f4fc6ee9b4 | ||
|
|
108c38d57b | ||
|
|
a1d73eb830 | ||
|
|
997906a610 | ||
|
|
b6e5d120d3 | ||
|
|
d469d0b435 | ||
|
|
e9f823e000 | ||
|
|
d7fb76ba74 | ||
|
|
b7dde1a0d9 | ||
|
|
15095d8c23 | ||
|
|
dfbebc7606 | ||
|
|
895309d93d | ||
|
|
bcf50e821a | ||
|
|
30195800dd | ||
|
|
6532b0f149 | ||
|
|
5e108e4057 | ||
|
|
c2b2f4d222 | ||
|
|
bc4329ad21 | ||
|
|
aec6d1b2f6 | ||
|
|
2baf119299 | ||
|
|
6fe4c5a2ed | ||
|
|
4abc8e41d8 | ||
|
|
af694f1ce9 | ||
|
|
7c3a5fcb83 | ||
|
|
57f64b18c6 | ||
|
|
4cccc7c2f8 | ||
|
|
903a2d6a6e | ||
|
|
34c674487a | ||
|
|
d15a8c5af3 | ||
|
|
3e0dec9383 | ||
|
|
8b810aad81 | ||
|
|
e676bcb4f4 | ||
|
|
a7aed77764 | ||
|
|
88875c0257 | ||
|
|
f711a0c91a | ||
|
|
d8a076cc6e | ||
|
|
c900831ee9 | ||
|
|
76a30c7ef4 | ||
|
|
ae5d0b1d81 | ||
|
|
cd5e87be34 | ||
|
|
3e967f58d2 | ||
|
|
1ea005ba7e | ||
|
|
092772ba90 | ||
|
|
b959854a76 | ||
|
|
8ccb1ebe4f | ||
|
|
91b3be6467 | ||
|
|
d79d5feacc | ||
|
|
5cc78ef9d5 | ||
|
|
8639cd5a72 | ||
|
|
021ddc17e7 | ||
|
|
ee47b8d004 | ||
|
|
55d267c935 | ||
|
|
0fd0b9128d | ||
|
|
d9cf505b50 | ||
|
|
6079332dda | ||
|
|
929ec20365 | ||
|
|
d0cad3055f | ||
|
|
4974a13bc0 | ||
|
|
bd048df225 | ||
|
|
ed83cbd574 | ||
|
|
7230207853 | ||
|
|
1ead8a72ab | ||
|
|
36a2e9d931 | ||
|
|
0f147a5518 | ||
|
|
fce511a18b | ||
|
|
64bb61b009 | ||
|
|
c6eefec5ce | ||
|
|
4c6f829c92 | ||
|
|
8c5cdd2acb | ||
|
|
e5357599c4 | ||
|
|
3800f19966 | ||
|
|
7336f84a4b | ||
|
|
7bf4a5b2b5 | ||
|
|
43a7b97218 | ||
|
|
9f95c57a09 | ||
|
|
8f6056ae66 | ||
|
|
9bcac6b10e | ||
|
|
86318e1b7d | ||
|
|
a8a1458833 | ||
|
|
942c1e2dfe | ||
|
|
a6b6814eae | ||
|
|
0af95aa9b1 | ||
|
|
b4b9256867 | ||
|
|
a6f1281a98 | ||
|
|
b54480928a | ||
|
|
741c74e267 | ||
|
|
3061dba5ed | ||
|
|
09f5f4027e | ||
|
|
925695fd56 | ||
|
|
3c758be856 | ||
|
|
569b76a7e3 | ||
|
|
dca69eff9c | ||
|
|
6b8fedc675 | ||
|
|
c42a379e7c | ||
|
|
a40858adbf | ||
|
|
19bc720bc9 | ||
|
|
bf79ca30bb | ||
|
|
75454895e5 | ||
|
|
c81aa2d6fe | ||
|
|
376f6369b8 | ||
|
|
b1e67a1ed3 | ||
|
|
7393a30bd1 | ||
|
|
c934065f8e | ||
|
|
56124d2b50 | ||
|
|
e8a003ff8a | ||
|
|
4c789225b2 | ||
|
|
59dcdd5393 | ||
|
|
b28316a4f2 | ||
|
|
4f44671acd | ||
|
|
b5eed69712 | ||
|
|
b79aacb2a7 | ||
|
|
8a2eb7b058 | ||
|
|
7316d076a2 | ||
|
|
479d3bcb40 | ||
|
|
f2358f1530 | ||
|
|
47d9e1b966 | ||
|
|
c53657d693 | ||
|
|
f19ce59e00 | ||
|
|
076f3e05d6 | ||
|
|
7d017f9494 | ||
|
|
675de4e420 | ||
|
|
418a709c6c | ||
|
|
1d7dd1b754 | ||
|
|
3fa70d6d2b | ||
|
|
9c67f52161 | ||
|
|
9f2f23fa96 | ||
|
|
46d955691a | ||
|
|
3f8800187d | ||
|
|
ebbe90dfa8 | ||
|
|
074f898160 | ||
|
|
a0e1783e18 | ||
|
|
fc83e11d8b | ||
|
|
f43627b170 | ||
|
|
8964441f44 | ||
|
|
cfd7a0c621 | ||
|
|
15a422873e | ||
|
|
d1f5583cd7 | ||
|
|
08f07c6f3e | ||
|
|
35a08debc3 | ||
|
|
a3424c480f | ||
|
|
118ced0a43 | ||
|
|
6d355ef0cd | ||
|
|
a8aa5ac231 | ||
|
|
df6bc0b3c9 | ||
|
|
6b965b765c | ||
|
|
d7aea6b5ba | ||
|
|
1e9a46855d | ||
|
|
91e9c18110 | ||
|
|
8ffa6088d7 | ||
|
|
52d2f8364f | ||
|
|
1f679af6fa | ||
|
|
1ba92cdcd5 | ||
|
|
45c60ba5f5 | ||
|
|
d3eef45608 | ||
|
|
1960c113d4 | ||
|
|
63d6b4a1c9 | ||
|
|
9f47bb1252 | ||
|
|
df4fea31d0 | ||
|
|
98ef1484c8 | ||
|
|
c4ef9960b9 | ||
|
|
6b6f7744aa | ||
|
|
9192fa0fe2 | ||
|
|
3c7c2dc1a5 | ||
|
|
5c176a1af0 | ||
|
|
6d03a1cc76 | ||
|
|
1cf10edef1 | ||
|
|
6a97c63bf4 | ||
|
|
15f9612bfa | ||
|
|
9a7c90b194 | ||
|
|
91f2708a87 | ||
|
|
7bf3ecd89d | ||
|
|
4768581631 | ||
|
|
aa4cd10e13 | ||
|
|
066396916d | ||
|
|
34ae57e6fe | ||
|
|
107c2b50e2 | ||
|
|
a832765203 | ||
|
|
977fee82b5 | ||
|
|
8c74cbc1c6 | ||
|
|
b38eec5039 | ||
|
|
6c20b932fa | ||
|
|
deb24c638f | ||
|
|
40fcdb4d28 | ||
|
|
f3e44cf458 | ||
|
|
498748217d | ||
|
|
483bf331fa | ||
|
|
9d62b4acdd | ||
|
|
c9deef6e76 | ||
|
|
8ba6f8b0e1 | ||
|
|
824cbdc84b | ||
|
|
448c59ea88 | ||
|
|
91b858bf33 | ||
|
|
c12bede980 | ||
|
|
71e9fa3d16 | ||
|
|
6800b9aaae | ||
|
|
77d44f25f9 | ||
|
|
ab6227828b | ||
|
|
719ba56c59 | ||
|
|
dacedf4018 | ||
|
|
2526fa3c47 | ||
|
|
7e2295c382 | ||
|
|
6ef02004ff | ||
|
|
0e60d062e9 | ||
|
|
80a94f97c4 | ||
|
|
c18bc5fe67 | ||
|
|
02b98a2429 | ||
|
|
0383aeaa87 | ||
|
|
15a41d532e | ||
|
|
0f49725789 | ||
|
|
1db6733e66 | ||
|
|
0343ee4f6b | ||
|
|
2c37d2233a | ||
|
|
0cb8ccfddd | ||
|
|
41c0e85d00 | ||
|
|
35b1a39ed8 | ||
|
|
61a577ba70 | ||
|
|
a1e32584fa | ||
|
|
28e0ee536d | ||
|
|
9d64a9c038 | ||
|
|
702ba969c2 | ||
|
|
6dde8ee2b8 | ||
|
|
018420310c | ||
|
|
6d49d34033 | ||
|
|
1fbd403164 | ||
|
|
13f544d2be | ||
|
|
3c9e64de81 | ||
|
|
5a9bafbc32 | ||
|
|
b89d96b66f | ||
|
|
b7176191ac | ||
|
|
453c5f47c2 | ||
|
|
eea62e1263 | ||
|
|
4fb2a0f1ca | ||
|
|
1d102ef096 | ||
|
|
bf3c65778e | ||
|
|
df7fe3e6b4 | ||
|
|
b657468b62 | ||
|
|
4edc0058d3 | ||
|
|
2c3b35293b | ||
|
|
be0c9a4d46 | ||
|
|
dd4140558e | ||
|
|
71c2519b8e | ||
|
|
badfc26aed | ||
|
|
b2bc3adb3d | ||
|
|
5ccf408fd6 | ||
|
|
da185875bb | ||
|
|
af16912541 | ||
|
|
1bf9e2a5e6 | ||
|
|
5a572651ff | ||
|
|
5a191e387f | ||
|
|
18f29f5790 | ||
|
|
054a73e0f8 | ||
|
|
14824db7b0 | ||
|
|
721c48ea88 | ||
|
|
ed7bfcfb58 | ||
|
|
773a40a126 | ||
|
|
961252ef26 | ||
|
|
a2650f3c47 | ||
|
|
d71ee194e1 | ||
|
|
22e1a4cf41 | ||
|
|
a50bf901d3 | ||
|
|
c9469635b5 | ||
|
|
36df3278e5 | ||
|
|
cb2258aaa8 | ||
|
|
0391d9eb7e | ||
|
|
12698b4c20 | ||
|
|
f7b9d459ab | ||
|
|
65ab14e68b | ||
|
|
93a5dd5de4 | ||
|
|
61807bdaaa | ||
|
|
a1a5d1adba | ||
|
|
9dd4aefea5 | ||
|
|
db4540089a | ||
|
|
24c899c91a | ||
|
|
ade1a73966 | ||
|
|
fb9ec2b040 | ||
|
|
3a683812e9 | ||
|
|
6d317603c9 | ||
|
|
5a3d2d196c | ||
|
|
e740c4d980 | ||
|
|
253e4596e2 | ||
|
|
70e75a355c | ||
|
|
4f885c9a79 | ||
|
|
b519d2afac | ||
|
|
6b61e3b76b | ||
|
|
30b9c72c31 | ||
|
|
385bf74f6e | ||
|
|
be5615e530 | ||
|
|
d81a03c093 | ||
|
|
f8249c8267 | ||
|
|
5a1cbdcd3b | ||
|
|
e0c99d87bd | ||
|
|
548250029d | ||
|
|
66a354dbdc | ||
|
|
834e602686 | ||
|
|
1f693ca4f6 | ||
|
|
97a0bc6045 | ||
|
|
8b75cdfefd | ||
|
|
917aecf1ff | ||
|
|
663dcd0396 | ||
|
|
8f2dffb1ad | ||
|
|
20228e3d19 | ||
|
|
81c6cc11b3 | ||
|
|
2ccacbe5f3 | ||
|
|
a5345e8468 | ||
|
|
8f5d62bb81 | ||
|
|
28f6838560 | ||
|
|
c86aacb31c | ||
|
|
f62f5192d6 | ||
|
|
b14ea1fe3e | ||
|
|
552633a00b | ||
|
|
7faba2a690 | ||
|
|
db910aff06 | ||
|
|
72126052ad | ||
|
|
75d9f6a7e7 | ||
|
|
de677294c6 | ||
|
|
da1e6b8259 | ||
|
|
a9633b3990 | ||
|
|
7ac9af1cc1 | ||
|
|
00d8b8cd61 | ||
|
|
af7ff7f5cf | ||
|
|
2a20719130 | ||
|
|
f481940180 | ||
|
|
ca8824d1e3 | ||
|
|
f4be199b77 | ||
|
|
6bcef8334e | ||
|
|
3955eff683 | ||
|
|
aa0f6ecd75 | ||
|
|
ef4a94ed78 | ||
|
|
b5c803ce65 | ||
|
|
d4325ed82e | ||
|
|
3805fb8f26 | ||
|
|
d4d938c655 | ||
|
|
1c6911e361 | ||
|
|
a1b364f337 | ||
|
|
ece5c3da86 | ||
|
|
5d1ae6047b | ||
|
|
5605c72253 | ||
|
|
66bbcf0733 | ||
|
|
acc23ea7bb | ||
|
|
663bd0c9f0 | ||
|
|
39b1025dfa | ||
|
|
d2875e90b2 | ||
|
|
ff461d1d02 | ||
|
|
58164ea2d3 | ||
|
|
1bf4834004 | ||
|
|
bf58d78281 | ||
|
|
0dc749bb3d | ||
|
|
a8aedfde55 | ||
|
|
b174a89032 | ||
|
|
9b92d1b673 | ||
|
|
febc9aed11 | ||
|
|
de2462677e | ||
|
|
8bd94d46eb | ||
|
|
d43cefe28f | ||
|
|
b82874e261 | ||
|
|
8554cb5d6c | ||
|
|
f901614056 | ||
|
|
b555d217ab | ||
|
|
775c600234 | ||
|
|
128f2570b8 | ||
|
|
3cd53e79b4 | ||
|
|
ebba84ffda | ||
|
|
1e1a42fe98 | ||
|
|
8a744a440d | ||
|
|
f4fc3c7d55 | ||
|
|
0594d121de | ||
|
|
12c85d6234 | ||
|
|
87d05223af | ||
|
|
babf6366e8 | ||
|
|
5e37728f66 | ||
|
|
e8e19fede7 | ||
|
|
e565dbfa66 | ||
|
|
d180d6820c | ||
|
|
7f252e9b7c | ||
|
|
41db8681f8 | ||
|
|
26cd58fd6d | ||
|
|
63c7e1aa9d | ||
|
|
d5a6063e5e | ||
|
|
00affdbdec | ||
|
|
db3f0bbd4f | ||
|
|
020a59cb97 | ||
|
|
ff4fa6402d |
@@ -1,11 +1,11 @@
|
||||
# pulls community scripts from git repo
|
||||
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
|
||||
FROM python:3.11.6-slim AS GET_SCRIPTS_STAGE
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git && \
|
||||
git clone https://github.com/amidaware/community-scripts.git /community-scripts
|
||||
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.11.6-slim
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
@@ -18,7 +18,7 @@ ENV PYTHONUNBUFFERED=1
|
||||
EXPOSE 8000 8383 8005
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
apt-get install -y build-essential weasyprint
|
||||
|
||||
RUN groupadd -g 1000 tactical && \
|
||||
useradd -u 1000 -g 1000 tactical
|
||||
@@ -27,7 +27,7 @@ RUN groupadd -g 1000 tactical && \
|
||||
COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts
|
||||
|
||||
# Copy dev python reqs
|
||||
COPY .devcontainer/requirements.txt /
|
||||
COPY .devcontainer/requirements.txt /
|
||||
|
||||
# Copy docker entrypoint.sh
|
||||
COPY .devcontainer/entrypoint.sh /
|
||||
|
||||
@@ -22,22 +22,6 @@ services:
|
||||
aliases:
|
||||
- tactical-backend
|
||||
|
||||
app-dev:
|
||||
container_name: trmm-app-dev
|
||||
image: node:16-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||
user: 1000:1000
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
ports:
|
||||
- "8080:${APP_PORT}"
|
||||
networks:
|
||||
dev:
|
||||
aliases:
|
||||
- tactical-frontend
|
||||
|
||||
# nats
|
||||
nats-dev:
|
||||
container_name: trmm-nats-dev
|
||||
@@ -232,6 +216,7 @@ services:
|
||||
- "443:4443"
|
||||
volumes:
|
||||
- tactical-data-dev:/opt/tactical
|
||||
- ..:/workspace:cached
|
||||
|
||||
volumes:
|
||||
tactical-data-dev: null
|
||||
|
||||
@@ -15,10 +15,7 @@ set -e
|
||||
: "${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}"
|
||||
|
||||
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
|
||||
@@ -81,6 +78,17 @@ DATABASES = {
|
||||
'PASSWORD': '${POSTGRES_PASS}',
|
||||
'HOST': '${POSTGRES_HOST}',
|
||||
'PORT': '${POSTGRES_PORT}',
|
||||
},
|
||||
'reporting': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': '${POSTGRES_DB}',
|
||||
'USER': 'reporting_user',
|
||||
'PASSWORD': 'read_password',
|
||||
'HOST': '${POSTGRES_HOST}',
|
||||
'PORT': '${POSTGRES_PORT}',
|
||||
'OPTIONS': {
|
||||
'options': '-c default_transaction_read_only=on'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +106,7 @@ EOF
|
||||
# run migrations and init scripts
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py generate_json_schemas
|
||||
"${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
|
||||
@@ -123,6 +132,8 @@ if [ "$1" = 'tactical-init-dev' ]; then
|
||||
mkdir -p /meshcentral-data
|
||||
mkdir -p ${TACTICAL_DIR}/tmp
|
||||
mkdir -p ${TACTICAL_DIR}/certs
|
||||
mkdir -p ${TACTICAL_DIR}/reporting
|
||||
mkdir -p ${TACTICAL_DIR}/reporting/assets
|
||||
mkdir -p /mongo/data/db
|
||||
mkdir -p /redis/data
|
||||
touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data
|
||||
@@ -130,6 +141,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
|
||||
touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs
|
||||
touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db
|
||||
touch /redis/data/.initialized && chown -R 1000:1000 /redis/data
|
||||
touch ${TACTICAL_DIR}/reporting && chown -R 1000:1000 ${TACTICAL_DIR}/reporting
|
||||
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe
|
||||
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log
|
||||
touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log
|
||||
@@ -142,16 +154,6 @@ if [ "$1" = 'tactical-init-dev' ]; then
|
||||
|
||||
django_setup
|
||||
|
||||
# create .env file for frontend
|
||||
webenv="$(cat << EOF
|
||||
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||
DEV_PORT = ${APP_PORT}
|
||||
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}"
|
||||
|
||||
@@ -1,41 +1,3 @@
|
||||
# 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
|
||||
asgiref==3.5.0
|
||||
celery==5.2.6
|
||||
channels==3.0.4
|
||||
channels_redis==3.4.0
|
||||
daphne==3.0.2
|
||||
Django==4.0.4
|
||||
django-cors-headers==3.11.0
|
||||
django-ipware==4.0.2
|
||||
django-rest-knox==4.2.0
|
||||
djangorestframework==3.13.1
|
||||
future==0.18.2
|
||||
msgpack==1.0.3
|
||||
nats-py==2.1.0
|
||||
packaging==21.3
|
||||
psycopg2-binary==2.9.3
|
||||
pycryptodome==3.14.1
|
||||
pyotp==2.6.0
|
||||
pytz==2022.1
|
||||
qrcode==7.3.1
|
||||
redis==4.2.2
|
||||
requests==2.27.1
|
||||
twilio==7.8.1
|
||||
urllib3==1.26.9
|
||||
validators==0.18.2
|
||||
websockets==10.2
|
||||
drf_spectacular==0.22.0
|
||||
meshctrl==0.1.15
|
||||
hiredis==2.0.0
|
||||
|
||||
# dev
|
||||
black==22.3.0
|
||||
django-extensions==3.1.5
|
||||
isort==5.10.1
|
||||
mypy==0.942
|
||||
types-pytz==2021.3.6
|
||||
model-bakery==1.5.0
|
||||
coverage==6.3.2
|
||||
django-silk==4.3.0
|
||||
django-stubs==1.10.1
|
||||
djangorestframework-stubs==1.5.0
|
||||
-r /workspace/api/tacticalrmm/requirements.txt
|
||||
-r /workspace/api/tacticalrmm/requirements-dev.txt
|
||||
-r /workspace/api/tacticalrmm/requirements-test.txt
|
||||
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,9 +1,9 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: wh1te909
|
||||
github: amidaware
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: tacticalrmm
|
||||
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
|
||||
|
||||
27
.github/workflows/ci-tests.yml
vendored
27
.github/workflows/ci-tests.yml
vendored
@@ -14,22 +14,23 @@ jobs:
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10.4']
|
||||
python-version: ["3.11.6"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: harmon758/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: '14'
|
||||
postgresql db: 'pipeline'
|
||||
postgresql user: 'pipeline'
|
||||
postgresql password: 'pipeline123456'
|
||||
|
||||
postgresql version: "15"
|
||||
postgresql db: "pipeline"
|
||||
postgresql user: "pipeline"
|
||||
postgresql password: "pipeline123456"
|
||||
|
||||
- name: Setup Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
- name: Install redis
|
||||
run: |
|
||||
@@ -49,13 +50,21 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
|
||||
- name: Codestyle black
|
||||
working-directory: api/tacticalrmm
|
||||
working-directory: api
|
||||
run: |
|
||||
black --exclude migrations/ --check tacticalrmm
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Lint with flake8
|
||||
working-directory: api/tacticalrmm
|
||||
run: |
|
||||
flake8 --config .flake8 .
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run django tests
|
||||
env:
|
||||
GHACTIONS: "yes"
|
||||
|
||||
34
.github/workflows/devskim-analysis.yml
vendored
34
.github/workflows/devskim-analysis.yml
vendored
@@ -1,34 +0,0 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: DevSkim
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
schedule:
|
||||
- cron: '19 5 * * 0'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run DevSkim scanner
|
||||
uses: microsoft/DevSkim-Action@v1
|
||||
|
||||
- name: Upload DevSkim scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: devskim-results.sarif
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -55,3 +55,7 @@ coverage.lcov
|
||||
daphne.sock.lock
|
||||
.pytest_cache
|
||||
coverage.xml
|
||||
setup_dev.yml
|
||||
11env/
|
||||
query_schema.json
|
||||
gunicorn_config.py
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
|
||||
"python.defaultInterpreterPath": "api/env/bin/python",
|
||||
"python.languageServer": "Pylance",
|
||||
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
@@ -21,12 +21,18 @@
|
||||
".vscode/*.py",
|
||||
"**env/**"
|
||||
],
|
||||
"python.formatting.provider": "black",
|
||||
"mypy.targets": ["api/tacticalrmm"],
|
||||
"mypy.runUsingActiveInterpreter": true,
|
||||
"python.formatting.provider": "none",
|
||||
//"mypy.targets": [
|
||||
//"api/tacticalrmm"
|
||||
//],
|
||||
//"mypy.runUsingActiveInterpreter": true,
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.formatOnSave": true,
|
||||
"files.associations": {
|
||||
"**/ansible/**/*.yml": "ansible",
|
||||
"**/docker/**/docker-compose*.yml": "dockercompose"
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
@@ -65,5 +71,8 @@
|
||||
"usePlaceholders": true,
|
||||
"completeUnimported": true,
|
||||
"staticcheck": true
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
|
||||
## Linux agent versions supported
|
||||
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
|
||||
|
||||
## Mac agent versions supported
|
||||
- 64 bit Intel and Apple Silicon (M1, M2)
|
||||
|
||||
## Installation / Backup / Restore / Usage
|
||||
|
||||
### Refer to the [documentation](https://docs.tacticalrmm.com)
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.12.2 | :white_check_mark: |
|
||||
| < 0.12.2 | :x: |
|
||||
[Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
3
ansible/README.md
Normal file
3
ansible/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### tacticalrmm ansible WIP
|
||||
|
||||
ansible role to setup a Debian 11 VM for tacticalrmm local development
|
||||
40
ansible/roles/trmm_dev/defaults/main.yml
Normal file
40
ansible/roles/trmm_dev/defaults/main.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
user: "tactical"
|
||||
python_ver: "3.11.6"
|
||||
go_ver: "1.20.7"
|
||||
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
|
||||
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
|
||||
scripts_repo: "https://github.com/amidaware/community-scripts.git"
|
||||
backend_dir: "/opt/trmm"
|
||||
frontend_dir: "/opt/trmm-web"
|
||||
scripts_dir: "/opt/trmm-community-scripts"
|
||||
trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm"
|
||||
mesh_dir: "/opt/meshcentral"
|
||||
settings_file: "{{ trmm_dir }}/settings.py"
|
||||
local_settings_file: "{{ trmm_dir }}/local_settings.py"
|
||||
fullchain_dest: /etc/ssl/certs/fullchain.pem
|
||||
privkey_dest: /etc/ssl/certs/privkey.pem
|
||||
|
||||
base_pkgs:
|
||||
- build-essential
|
||||
- curl
|
||||
- wget
|
||||
- dirmngr
|
||||
- gnupg
|
||||
- openssl
|
||||
- gcc
|
||||
- g++
|
||||
- make
|
||||
- ca-certificates
|
||||
- git
|
||||
|
||||
python_pkgs:
|
||||
- zlib1g-dev
|
||||
- libncurses5-dev
|
||||
- libgdbm-dev
|
||||
- libnss3-dev
|
||||
- libssl-dev
|
||||
- libreadline-dev
|
||||
- libffi-dev
|
||||
- libsqlite3-dev
|
||||
- libbz2-dev
|
||||
31
ansible/roles/trmm_dev/files/nginx-default.conf
Normal file
31
ansible/roles/trmm_dev/files/nginx-default.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
worker_rlimit_nofile 1000000;
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
server_tokens off;
|
||||
tcp_nopush on;
|
||||
types_hash_max_size 2048;
|
||||
server_names_hash_bucket_size 64;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
gzip on;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
20
ansible/roles/trmm_dev/files/vimrc.local
Normal file
20
ansible/roles/trmm_dev/files/vimrc.local
Normal file
@@ -0,0 +1,20 @@
|
||||
" This file loads the default vim options at the beginning and prevents
|
||||
" that they are being loaded again later. All other options that will be set,
|
||||
" are added, or overwrite the default settings. Add as many options as you
|
||||
" whish at the end of this file.
|
||||
|
||||
" Load the defaults
|
||||
source $VIMRUNTIME/defaults.vim
|
||||
|
||||
" Prevent the defaults from being loaded again later, if the user doesn't
|
||||
" have a local vimrc (~/.vimrc)
|
||||
let skip_defaults_vim = 1
|
||||
|
||||
|
||||
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
|
||||
" Add as many options as you whish
|
||||
|
||||
" Set the mouse mode to 'r'
|
||||
if has('mouse')
|
||||
set mouse=r
|
||||
endif
|
||||
634
ansible/roles/trmm_dev/tasks/main.yml
Normal file
634
ansible/roles/trmm_dev/tasks/main.yml
Normal file
@@ -0,0 +1,634 @@
|
||||
---
|
||||
- name: Append subdomains to hosts
|
||||
tags: hosts
|
||||
become: yes
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
backrefs: yes
|
||||
regexp: '^(127\.0\.1\.1 .*)$'
|
||||
line: "\\1 {{ api }} {{ mesh }} {{ rmm }}"
|
||||
|
||||
- name: set mouse mode for vim
|
||||
tags: vim
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: vimrc.local
|
||||
dest: /etc/vim/vimrc.local
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: set max_user_watches
|
||||
tags: sysctl
|
||||
become: yes
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sysctl.conf
|
||||
line: fs.inotify.max_user_watches=524288
|
||||
|
||||
- name: reload sysctl
|
||||
tags: sysctl
|
||||
become: yes
|
||||
ansible.builtin.command:
|
||||
cmd: sysctl -p
|
||||
|
||||
- name: install base packages
|
||||
tags: base
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: "{{ item }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
with_items:
|
||||
- "{{ base_pkgs }}"
|
||||
|
||||
- name: set arch fact
|
||||
ansible.builtin.set_fact:
|
||||
goarch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
|
||||
|
||||
- name: download and install golang
|
||||
tags: golang
|
||||
become: yes
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://go.dev/dl/go{{ go_ver }}.linux-{{ goarch }}.tar.gz"
|
||||
dest: /usr/local
|
||||
remote_src: yes
|
||||
|
||||
- name: add golang to path
|
||||
become: yes
|
||||
tags: golang
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/profile.d/golang.sh
|
||||
content: "PATH=$PATH:/usr/local/go/bin"
|
||||
|
||||
- name: install python prereqs
|
||||
tags: python
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- "{{ python_pkgs }}"
|
||||
|
||||
- name: get cpu core count
|
||||
tags: python
|
||||
ansible.builtin.command: nproc
|
||||
register: numprocs
|
||||
|
||||
- name: Create python tmpdir
|
||||
tags: python
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: python
|
||||
register: python_tmp
|
||||
|
||||
- name: download and extract python
|
||||
tags: python
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
|
||||
dest: "{{ python_tmp.path }}"
|
||||
remote_src: yes
|
||||
|
||||
- name: compile python
|
||||
tags: python
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
|
||||
cmd: |
|
||||
./configure --enable-optimizations
|
||||
make -j {{ numprocs.stdout }}
|
||||
|
||||
- name: alt install python
|
||||
tags: python
|
||||
become: yes
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
|
||||
cmd: |
|
||||
make altinstall
|
||||
|
||||
- name: install redis
|
||||
tags: redis
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: redis
|
||||
state: present
|
||||
|
||||
- name: create postgres repo
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
content: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
|
||||
dest: /etc/apt/sources.list.d/pgdg.list
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: import postgres repo signing key
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.apt_key:
|
||||
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
state: present
|
||||
|
||||
- name: install postgresql
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: postgresql-15
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: ensure postgres enabled and started
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: postgresql
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: setup trmm database
|
||||
tags: postgres
|
||||
become: yes
|
||||
become_user: postgres
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
psql -c "CREATE DATABASE tacticalrmm"
|
||||
psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
|
||||
psql -c "ALTER ROLE {{ db_user }} CREATEDB"
|
||||
psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
|
||||
psql -c "ALTER DATABASE tacticalrmm OWNER TO {{ db_user }}"
|
||||
psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ db_user }}"
|
||||
|
||||
- name: setup mesh database
|
||||
tags: postgres
|
||||
become: yes
|
||||
become_user: postgres
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
psql -c "CREATE DATABASE meshcentral"
|
||||
psql -c "CREATE USER {{ mesh_db_user }} WITH PASSWORD '{{ mesh_db_passwd }}'"
|
||||
psql -c "ALTER ROLE {{ mesh_db_user }} SET client_encoding TO 'utf8'"
|
||||
psql -c "ALTER ROLE {{ mesh_db_user }} SET default_transaction_isolation TO 'read committed'"
|
||||
psql -c "ALTER ROLE {{ mesh_db_user }} SET timezone TO 'UTC'"
|
||||
psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO {{ mesh_db_user }}"
|
||||
psql -c "ALTER DATABASE meshcentral OWNER TO {{ mesh_db_user }}"
|
||||
psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ mesh_db_user }}"
|
||||
|
||||
- name: create repo dirs
|
||||
become: yes
|
||||
tags: git
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
with_items:
|
||||
- "{{ backend_dir }}"
|
||||
- "{{ frontend_dir }}"
|
||||
- "{{ scripts_dir }}"
|
||||
|
||||
- name: git clone repos
|
||||
tags: git
|
||||
ansible.builtin.git:
|
||||
repo: "{{ item.repo }}"
|
||||
dest: "{{ item.dest }}"
|
||||
version: "{{ item.version }}"
|
||||
with_items:
|
||||
- {
|
||||
repo: "{{ backend_repo }}",
|
||||
dest: "{{ backend_dir }}",
|
||||
version: develop,
|
||||
}
|
||||
- {
|
||||
repo: "{{ frontend_repo }}",
|
||||
dest: "{{ frontend_dir }}",
|
||||
version: develop,
|
||||
}
|
||||
- { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
|
||||
|
||||
- name: get nats_server_ver
|
||||
tags: nats
|
||||
ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
|
||||
register: nats_server_ver
|
||||
|
||||
- name: Create nats tmpdir
|
||||
tags: nats
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: nats
|
||||
register: nats_tmp
|
||||
|
||||
- name: download and extract nats
|
||||
tags: nats
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}.tar.gz"
|
||||
dest: "{{ nats_tmp.path }}"
|
||||
remote_src: yes
|
||||
|
||||
- name: install nats
|
||||
tags: nats
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
remote_src: yes
|
||||
src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}/nats-server"
|
||||
dest: /usr/local/bin/nats-server
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Create nodejs tmpdir
|
||||
tags: nodejs
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: nodejs
|
||||
register: nodejs_tmp
|
||||
|
||||
- name: download nodejs setup
|
||||
tags: nodejs
|
||||
ansible.builtin.get_url:
|
||||
url: https://deb.nodesource.com/setup_18.x
|
||||
dest: "{{ nodejs_tmp.path }}/setup_node.sh"
|
||||
mode: "0755"
|
||||
|
||||
- name: run node setup script
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
|
||||
|
||||
- name: install nodejs
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: nodejs
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: update npm
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.shell:
|
||||
cmd: npm install -g npm
|
||||
|
||||
- name: install quasar cli
|
||||
tags: quasar
|
||||
become: yes
|
||||
ansible.builtin.shell:
|
||||
cmd: npm install -g @quasar/cli
|
||||
|
||||
- name: install frontend
|
||||
tags: quasar
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ frontend_dir }}"
|
||||
cmd: npm install
|
||||
|
||||
- name: add quasar env
|
||||
tags: quasar
|
||||
ansible.builtin.template:
|
||||
src: quasar.env.j2
|
||||
dest: "{{ frontend_dir }}/.env"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: remove tempdirs
|
||||
tags: cleanup
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- "{{ nats_tmp.path }}"
|
||||
- "{{ python_tmp.path }}"
|
||||
- "{{ nodejs_tmp.path }}"
|
||||
|
||||
- name: deploy fullchain
|
||||
tags: certs
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: "{{ fullchain_src }}"
|
||||
dest: "{{ fullchain_dest }}"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0440"
|
||||
|
||||
- name: deploy privkey
|
||||
tags: certs
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: "{{ privkey_src }}"
|
||||
dest: "{{ privkey_dest }}"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0440"
|
||||
|
||||
- name: import nginx signing key
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.apt_key:
|
||||
url: https://nginx.org/packages/keys/nginx_signing.key
|
||||
state: present
|
||||
|
||||
- name: add nginx repo
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.template:
|
||||
src: nginx.repo.j2
|
||||
dest: /etc/apt/sources.list.d/nginx.list
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: install nginx
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: nginx
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: set nginx default conf
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: nginx-default.conf
|
||||
dest: /etc/nginx/nginx.conf
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: create nginx dirs
|
||||
become: yes
|
||||
tags: nginx
|
||||
ansible.builtin.file:
|
||||
state: directory
|
||||
path: "{{ item }}"
|
||||
mode: "0755"
|
||||
with_items:
|
||||
- /etc/nginx/sites-available
|
||||
- /etc/nginx/sites-enabled
|
||||
|
||||
- name: deploy nginx sites
|
||||
become: yes
|
||||
tags: nginx
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
with_items:
|
||||
- { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf }
|
||||
- { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf }
|
||||
|
||||
- name: enable nginx sites
|
||||
become: yes
|
||||
tags: nginx
|
||||
ansible.builtin.file:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
state: link
|
||||
with_items:
|
||||
- {
|
||||
src: /etc/nginx/sites-available/backend.conf,
|
||||
dest: /etc/nginx/sites-enabled/backend.conf,
|
||||
}
|
||||
- {
|
||||
src: /etc/nginx/sites-available/mesh.conf,
|
||||
dest: /etc/nginx/sites-enabled/mesh.conf,
|
||||
}
|
||||
|
||||
- name: ensure nginx enabled and restarted
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: nginx
|
||||
enabled: yes
|
||||
state: restarted
|
||||
|
||||
- name: set natsapi fact
|
||||
ansible.builtin.set_fact:
|
||||
natsapi: "{{ 'nats-api' if ansible_architecture == 'x86_64' else 'nats-api-arm64' }}"
|
||||
|
||||
- name: copy nats-api bin
|
||||
tags: nats-api
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
remote_src: yes
|
||||
src: "{{ backend_dir }}/natsapi/bin/{{ natsapi }}"
|
||||
dest: /usr/local/bin/nats-api
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: get setuptools_ver
|
||||
tags: pip
|
||||
ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
|
||||
register: setuptools_ver
|
||||
|
||||
- name: get wheel_ver
|
||||
tags: pip
|
||||
ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
|
||||
register: wheel_ver
|
||||
|
||||
- name: setup virtual env
|
||||
tags: pip
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ backend_dir }}/api"
|
||||
cmd: python3.11 -m venv env
|
||||
|
||||
- name: update pip to latest
|
||||
tags: pip
|
||||
ansible.builtin.pip:
|
||||
virtualenv: "{{ backend_dir }}/api/env"
|
||||
name: pip
|
||||
state: latest
|
||||
|
||||
- name: install setuptools and wheel
|
||||
tags: pip
|
||||
ansible.builtin.pip:
|
||||
virtualenv: "{{ backend_dir }}/api/env"
|
||||
name: "{{ item }}"
|
||||
with_items:
|
||||
- "setuptools=={{ setuptools_ver.stdout }}"
|
||||
- "wheel=={{ wheel_ver.stdout }}"
|
||||
|
||||
- name: install python packages
|
||||
tags: pip
|
||||
ansible.builtin.pip:
|
||||
virtualenv: "{{ backend_dir }}/api/env"
|
||||
chdir: "{{ backend_dir }}/api/tacticalrmm"
|
||||
requirements: "{{ item }}"
|
||||
with_items:
|
||||
- requirements.txt
|
||||
- requirements-dev.txt
|
||||
- requirements-test.txt
|
||||
|
||||
- name: deploy django local settings
|
||||
tags: django
|
||||
ansible.builtin.template:
|
||||
src: local_settings.j2
|
||||
dest: "{{ local_settings_file }}"
|
||||
mode: "0644"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
|
||||
- name: setup django
|
||||
tags: django
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ backend_dir }}/api/tacticalrmm"
|
||||
cmd: |
|
||||
. ../env/bin/activate
|
||||
python manage.py migrate --no-input
|
||||
python manage.py collectstatic --no-input
|
||||
python manage.py create_natsapi_conf
|
||||
python manage.py load_chocos
|
||||
python manage.py load_community_scripts
|
||||
echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell
|
||||
python manage.py create_installer_user
|
||||
|
||||
- name: deploy services
|
||||
tags: services
|
||||
become: yes
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "0644"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
with_items:
|
||||
- { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service }
|
||||
- { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
|
||||
- { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
|
||||
|
||||
- name: get mesh_ver
|
||||
tags: mesh
|
||||
ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
|
||||
register: mesh_ver
|
||||
|
||||
- name: create meshcentral data directory
|
||||
tags: mesh
|
||||
become: yes
|
||||
ansible.builtin.file:
|
||||
path: "{{ mesh_dir }}/meshcentral-data"
|
||||
state: directory
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: install meshcentral
|
||||
tags: mesh
|
||||
ansible.builtin.command:
|
||||
chdir: "{{ mesh_dir }}"
|
||||
cmd: "npm install meshcentral@{{ mesh_ver.stdout }}"
|
||||
|
||||
- name: deploy mesh config
|
||||
tags: mesh
|
||||
ansible.builtin.template:
|
||||
src: mesh.cfg.j2
|
||||
dest: "{{ mesh_dir }}/meshcentral-data/config.json"
|
||||
mode: "0644"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
|
||||
- name: start meshcentral
|
||||
tags: mesh
|
||||
become: yes
|
||||
ansible.builtin.systemd:
|
||||
name: meshcentral.service
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: wait for meshcentral to be ready
|
||||
tags: mesh
|
||||
uri:
|
||||
url: "https://{{ mesh }}"
|
||||
return_content: yes
|
||||
validate_certs: yes
|
||||
status_code: 200
|
||||
register: mesh_status
|
||||
until: mesh_status.status == 200
|
||||
retries: 20
|
||||
delay: 3
|
||||
|
||||
- name: get meshcentral login token key
|
||||
tags: mesh_key
|
||||
ansible.builtin.command:
|
||||
chdir: "{{ mesh_dir }}"
|
||||
cmd: node node_modules/meshcentral --logintokenkey
|
||||
register: mesh_token_key
|
||||
|
||||
- name: add mesh key to django settings file
|
||||
tags: mesh_key
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ local_settings_file }}"
|
||||
line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"'
|
||||
|
||||
- name: stop meshcentral service
|
||||
tags: mesh_user
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: meshcentral.service
|
||||
state: stopped
|
||||
|
||||
- name: create mesh user
|
||||
tags: mesh_user
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ mesh_dir }}"
|
||||
cmd: |
|
||||
node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }}
|
||||
node node_modules/meshcentral --adminaccount {{ mesh_user }}
|
||||
|
||||
- name: start meshcentral service
|
||||
tags: mesh_user
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: meshcentral.service
|
||||
state: started
|
||||
|
||||
- name: wait for meshcentral to be ready
|
||||
tags: mesh_user
|
||||
uri:
|
||||
url: "https://{{ mesh }}"
|
||||
return_content: yes
|
||||
validate_certs: yes
|
||||
status_code: 200
|
||||
register: mesh_status
|
||||
until: mesh_status.status == 200
|
||||
retries: 20
|
||||
delay: 3
|
||||
|
||||
- name: create mesh device group
|
||||
tags: mesh_user
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ mesh_dir }}"
|
||||
cmd: |
|
||||
node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM
|
||||
|
||||
- name: finish up django
|
||||
tags: mesh_user
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ backend_dir }}/api/tacticalrmm"
|
||||
cmd: |
|
||||
. ../env/bin/activate
|
||||
python manage.py initial_db_setup
|
||||
python manage.py reload_nats
|
||||
|
||||
- name: restart services
|
||||
tags: services
|
||||
become: yes
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: yes
|
||||
enabled: yes
|
||||
state: restarted
|
||||
name: "{{ item }}.service"
|
||||
with_items:
|
||||
- nats
|
||||
- nats-api
|
||||
20
ansible/roles/trmm_dev/templates/backend.nginx.j2
Normal file
20
ansible/roles/trmm_dev/templates/backend.nginx.j2
Normal file
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 443 ssl reuseport;
|
||||
listen [::]:443 ssl;
|
||||
server_name {{ api }};
|
||||
client_max_body_size 300M;
|
||||
ssl_certificate {{ fullchain_dest }};
|
||||
ssl_certificate_key {{ privkey_dest }};
|
||||
|
||||
|
||||
location ~ ^/natsws {
|
||||
proxy_pass http://127.0.0.1:9235;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
21
ansible/roles/trmm_dev/templates/local_settings.j2
Normal file
21
ansible/roles/trmm_dev/templates/local_settings.j2
Normal file
@@ -0,0 +1,21 @@
|
||||
SECRET_KEY = "{{ django_secret }}"
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['{{ api }}']
|
||||
ADMIN_URL = "admin/"
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'tacticalrmm',
|
||||
'USER': '{{ db_user }}',
|
||||
'PASSWORD': '{{ db_passwd }}',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
REDIS_HOST = "localhost"
|
||||
ADMIN_ENABLED = True
|
||||
CERT_FILE = "{{ fullchain_dest }}"
|
||||
KEY_FILE = "{{ privkey_dest }}"
|
||||
MESH_USERNAME = "{{ mesh_user }}"
|
||||
MESH_SITE = "https://{{ mesh }}"
|
||||
37
ansible/roles/trmm_dev/templates/mesh.cfg.j2
Normal file
37
ansible/roles/trmm_dev/templates/mesh.cfg.j2
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"settings": {
|
||||
"Cert": "{{ mesh }}",
|
||||
"WANonly": true,
|
||||
"Minify": 1,
|
||||
"Port": 4430,
|
||||
"AliasPort": 443,
|
||||
"RedirPort": 800,
|
||||
"AllowLoginToken": true,
|
||||
"AllowFraming": true,
|
||||
"AgentPing": 35,
|
||||
"AllowHighQualityDesktop": true,
|
||||
"TlsOffload": "127.0.0.1",
|
||||
"agentCoreDump": false,
|
||||
"Compression": true,
|
||||
"WsCompression": true,
|
||||
"AgentWsCompression": true,
|
||||
"MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 },
|
||||
"postgres": {
|
||||
"user": "{{ mesh_db_user }}",
|
||||
"password": "{{ mesh_db_passwd }}",
|
||||
"port": "5432",
|
||||
"host": "localhost"
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"": {
|
||||
"Title": "Tactical RMM Dev",
|
||||
"Title2": "Tactical RMM Dev",
|
||||
"NewAccounts": false,
|
||||
"CertUrl": "https://{{ mesh }}:443/",
|
||||
"GeoLocation": true,
|
||||
"CookieIpCheck": false,
|
||||
"mstsc": true
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ansible/roles/trmm_dev/templates/mesh.nginx.j2
Normal file
22
ansible/roles/trmm_dev/templates/mesh.nginx.j2
Normal file
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
proxy_send_timeout 330s;
|
||||
proxy_read_timeout 330s;
|
||||
server_name {{ mesh }};
|
||||
ssl_certificate {{ fullchain_dest }};
|
||||
ssl_certificate_key {{ privkey_dest }};
|
||||
|
||||
ssl_session_cache shared:WEBSSL:10m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4430/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
17
ansible/roles/trmm_dev/templates/mesh.systemd.j2
Normal file
17
ansible/roles/trmm_dev/templates/mesh.systemd.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=MeshCentral Server
|
||||
After=network.target postgresql.service nginx.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
LimitNOFILE=1000000
|
||||
ExecStart=/usr/bin/node node_modules/meshcentral
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory={{ mesh_dir }}
|
||||
User={{ user }}
|
||||
Group={{ user }}
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
14
ansible/roles/trmm_dev/templates/nats-api.systemd.j2
Normal file
14
ansible/roles/trmm_dev/templates/nats-api.systemd.j2
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=TacticalRMM Nats Api
|
||||
After=nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf
|
||||
User={{ user }}
|
||||
Group={{ user }}
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
18
ansible/roles/trmm_dev/templates/nats-server.systemd.j2
Normal file
18
ansible/roles/trmm_dev/templates/nats-server.systemd.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=NATS Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
PrivateTmp=true
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf
|
||||
ExecReload=/usr/bin/kill -s HUP $MAINPID
|
||||
ExecStop=/usr/bin/kill -s SIGINT $MAINPID
|
||||
User={{ user }}
|
||||
Group={{ user }}
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
LimitNOFILE=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
2
ansible/roles/trmm_dev/templates/nginx.repo.j2
Normal file
2
ansible/roles/trmm_dev/templates/nginx.repo.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
deb https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx
|
||||
deb-src https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx
|
||||
4
ansible/roles/trmm_dev/templates/quasar.env.j2
Normal file
4
ansible/roles/trmm_dev/templates/quasar.env.j2
Normal file
@@ -0,0 +1,4 @@
|
||||
DEV_URL = "http://{{ api }}:8000"
|
||||
DEV_HOST = "0.0.0.0"
|
||||
DEV_PORT = "8080"
|
||||
USE_HTTPS = false
|
||||
22
ansible/setup_dev.yml.example
Normal file
22
ansible/setup_dev.yml.example
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
- hosts: "{{ target }}"
|
||||
vars:
|
||||
ansible_user: tactical
|
||||
fullchain_src: /path/to/fullchain.pem
|
||||
privkey_src: /path/to/privkey.pem
|
||||
api: "api.example.com"
|
||||
rmm: "rmm.example.com"
|
||||
mesh: "mesh.example.com"
|
||||
github_username: "changeme"
|
||||
github_email: "changeme@example.com"
|
||||
mesh_user: "changeme"
|
||||
mesh_password: "changeme"
|
||||
db_user: "changeme"
|
||||
db_passwd: "changeme"
|
||||
mesh_db_user: "changeme"
|
||||
mesh_db_passwd: "changeme"
|
||||
django_secret: "changeme"
|
||||
django_user: "changeme"
|
||||
django_password: "changeme"
|
||||
roles:
|
||||
- trmm_dev
|
||||
12
api/tacticalrmm/.flake8
Normal file
12
api/tacticalrmm/.flake8
Normal file
@@ -0,0 +1,12 @@
|
||||
[flake8]
|
||||
ignore = E501,W503,E722,E203
|
||||
exclude =
|
||||
.mypy*
|
||||
.pytest*
|
||||
.git
|
||||
demo_data.py
|
||||
manage.py
|
||||
*/__pycache__/*
|
||||
*/env/*
|
||||
/usr/local/lib/*
|
||||
**/migrations/*
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
from tacticalrmm.helpers import make_random_password
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -17,7 +18,7 @@ class Command(BaseCommand):
|
||||
User.objects.create_user(
|
||||
username=uuid.uuid4().hex,
|
||||
is_installer_user=True,
|
||||
password=User.objects.make_random_password(60),
|
||||
password=make_random_password(len=60),
|
||||
block_dashboard_login=True,
|
||||
)
|
||||
self.stdout.write("Installer user has been created")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pyotp
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
from tacticalrmm.helpers import get_webdomain
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -21,28 +21,13 @@ class Command(BaseCommand):
|
||||
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)
|
||||
url = pyotp.totp.TOTP(code).provisioning_uri(
|
||||
username, issuer_name=get_webdomain()
|
||||
)
|
||||
subprocess.run(f'qr "{url}"', shell=True)
|
||||
self.stdout.write(
|
||||
self.style.WARNING("Scan the barcode above with your authenticator app")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from getpass import getpass
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
@@ -17,7 +19,13 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
|
||||
return
|
||||
|
||||
passwd = input("Enter new password: ")
|
||||
user.set_password(passwd)
|
||||
pass1, pass2 = "foo", "bar"
|
||||
while pass1 != pass2:
|
||||
pass1 = getpass()
|
||||
pass2 = getpass(prompt="Confirm Password:")
|
||||
if pass1 != pass2:
|
||||
self.stdout.write(self.style.ERROR("Passwords don't match"))
|
||||
|
||||
user.set_password(pass1)
|
||||
user.save()
|
||||
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-17 07:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0031_user_date_format"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="default_agent_tbl_tab",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("server", "Servers"),
|
||||
("workstation", "Workstations"),
|
||||
("mixed", "Mixed"),
|
||||
],
|
||||
default="mixed",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-23 04:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0032_alter_user_default_agent_tbl_tab"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="dash_info_color",
|
||||
field=models.CharField(default="info", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="dash_negative_color",
|
||||
field=models.CharField(default="negative", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="dash_positive_color",
|
||||
field=models.CharField(default="positive", max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="dash_warning_color",
|
||||
field=models.CharField(default="warning", max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.9 on 2023-05-26 23:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0033_user_dash_info_color_user_dash_negative_color_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="can_send_wol",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-08 22:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0034_role_can_send_wol"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="can_manage_reports",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="can_view_reports",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -31,7 +31,7 @@ class User(AbstractUser, BaseAuditModel):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
default_agent_tbl_tab = models.CharField(
|
||||
max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.SERVER
|
||||
max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.MIXED
|
||||
)
|
||||
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
|
||||
client_tree_sort = models.CharField(
|
||||
@@ -39,6 +39,10 @@ class User(AbstractUser, BaseAuditModel):
|
||||
)
|
||||
client_tree_splitter = models.PositiveIntegerField(default=11)
|
||||
loading_bar_color = models.CharField(max_length=255, default="red")
|
||||
dash_info_color = models.CharField(max_length=255, default="info")
|
||||
dash_positive_color = models.CharField(max_length=255, default="positive")
|
||||
dash_negative_color = models.CharField(max_length=255, default="negative")
|
||||
dash_warning_color = models.CharField(max_length=255, default="warning")
|
||||
clear_search_when_switching = models.BooleanField(default=True)
|
||||
date_format = models.CharField(max_length=30, blank=True, null=True)
|
||||
is_installer_user = models.BooleanField(default=False)
|
||||
@@ -105,6 +109,7 @@ class Role(BaseAuditModel):
|
||||
can_run_bulk = models.BooleanField(default=False)
|
||||
can_recover_agents = models.BooleanField(default=False)
|
||||
can_list_agent_history = models.BooleanField(default=False)
|
||||
can_send_wol = models.BooleanField(default=False)
|
||||
|
||||
# core
|
||||
can_list_notes = models.BooleanField(default=False)
|
||||
@@ -181,11 +186,14 @@ class Role(BaseAuditModel):
|
||||
can_list_api_keys = models.BooleanField(default=False)
|
||||
can_manage_api_keys = models.BooleanField(default=False)
|
||||
|
||||
# reporting
|
||||
can_view_reports = models.BooleanField(default=False)
|
||||
can_manage_reports = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
|
||||
# delete cache on save
|
||||
cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
@@ -7,32 +7,31 @@ class AccountsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_accounts")
|
||||
else:
|
||||
|
||||
# allow users to reset their own password/2fa see issue #686
|
||||
base_path = "/accounts/users/"
|
||||
paths = ["reset/", "reset_totp/"]
|
||||
# allow users to reset their own password/2fa see issue #686
|
||||
base_path = "/accounts/users/"
|
||||
paths = ("reset/", "reset_totp/")
|
||||
|
||||
if r.path in [base_path + i for i in paths]:
|
||||
from accounts.models import User
|
||||
if r.path in [base_path + i for i in paths]:
|
||||
from accounts.models import User
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=r.data["id"])
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if user == r.user:
|
||||
return True
|
||||
try:
|
||||
user = User.objects.get(pk=r.data["id"])
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if user == r.user:
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_accounts")
|
||||
return _has_perm(r, "can_manage_accounts")
|
||||
|
||||
|
||||
class RolesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_roles")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_roles")
|
||||
|
||||
return _has_perm(r, "can_manage_roles")
|
||||
|
||||
|
||||
class APIKeyPerms(permissions.BasePermission):
|
||||
|
||||
@@ -5,6 +5,8 @@ from rest_framework.serializers import (
|
||||
SerializerMethodField,
|
||||
)
|
||||
|
||||
from tacticalrmm.helpers import get_webdomain
|
||||
|
||||
from .models import APIKey, Role, User
|
||||
|
||||
|
||||
@@ -20,6 +22,10 @@ class UserUISerializer(ModelSerializer):
|
||||
"client_tree_sort",
|
||||
"client_tree_splitter",
|
||||
"loading_bar_color",
|
||||
"dash_info_color",
|
||||
"dash_positive_color",
|
||||
"dash_negative_color",
|
||||
"dash_warning_color",
|
||||
"clear_search_when_switching",
|
||||
"block_dashboard_login",
|
||||
"date_format",
|
||||
@@ -45,7 +51,6 @@ class UserSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class TOTPSetupSerializer(ModelSerializer):
|
||||
|
||||
qr_url = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
@@ -58,7 +63,7 @@ class TOTPSetupSerializer(ModelSerializer):
|
||||
|
||||
def get_qr_url(self, obj):
|
||||
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
|
||||
obj.username, issuer_name="Tactical RMM"
|
||||
obj.username, issuer_name=get_webdomain()
|
||||
)
|
||||
|
||||
|
||||
@@ -80,7 +85,6 @@ class RoleAuditSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class APIKeySerializer(ModelSerializer):
|
||||
|
||||
username = ReadOnlyField(source="user.username")
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -197,7 +197,7 @@ class GetUpdateDeleteUser(TacticalTestCase):
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
url = f"/accounts/893452/users/"
|
||||
url = "/accounts/893452/users/"
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
@@ -297,6 +297,27 @@ class TestUserAction(TacticalTestCase):
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
|
||||
class TestUserReset(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_reset_pw(self):
|
||||
url = "/accounts/resetpw/"
|
||||
data = {"password": "superSekret123456"}
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_reset_2fa(self):
|
||||
url = "/accounts/reset2fa/"
|
||||
r = self.client.put(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
|
||||
class TestAPIKeyViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
|
||||
@@ -13,4 +13,6 @@ urlpatterns = [
|
||||
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
|
||||
path("apikeys/", views.GetAddAPIKeys.as_view()),
|
||||
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
|
||||
path("resetpw/", views.ResetPass.as_view()),
|
||||
path("reset2fa/", views.Reset2FA.as_view()),
|
||||
]
|
||||
|
||||
18
api/tacticalrmm/accounts/utils.py
Normal file
18
api/tacticalrmm/accounts/utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from django.conf import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import HttpRequest
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
|
||||
root = (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
)
|
||||
demo = (
|
||||
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
|
||||
)
|
||||
return root or demo
|
||||
@@ -22,26 +22,13 @@ from .serializers import (
|
||||
UserSerializer,
|
||||
UserUISerializer,
|
||||
)
|
||||
|
||||
|
||||
def _is_root_user(request, user) -> bool:
|
||||
root = (
|
||||
hasattr(settings, "ROOT_USER")
|
||||
and request.user != user
|
||||
and user.username == settings.ROOT_USER
|
||||
)
|
||||
demo = (
|
||||
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
|
||||
)
|
||||
return root or demo
|
||||
from accounts.utils import is_root_user
|
||||
|
||||
|
||||
class CheckCreds(KnoxLoginView):
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
|
||||
# check credentials
|
||||
serializer = AuthTokenSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
@@ -66,7 +53,6 @@ class CheckCreds(KnoxLoginView):
|
||||
|
||||
|
||||
class LoginView(KnoxLoginView):
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
@@ -93,7 +79,7 @@ class LoginView(KnoxLoginView):
|
||||
login(request, user)
|
||||
|
||||
# save ip information
|
||||
client_ip, is_routable = get_client_ip(request)
|
||||
client_ip, _ = get_client_ip(request)
|
||||
user.last_login_ip = client_ip
|
||||
user.save()
|
||||
|
||||
@@ -159,7 +145,7 @@ class GetUpdateDeleteUser(APIView):
|
||||
def put(self, request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
if _is_root_user(request, user):
|
||||
if is_root_user(request=request, user=user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
||||
@@ -170,7 +156,7 @@ class GetUpdateDeleteUser(APIView):
|
||||
|
||||
def delete(self, request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
if _is_root_user(request, user):
|
||||
if is_root_user(request=request, user=user):
|
||||
return notify_error("The root user cannot be deleted from the UI")
|
||||
|
||||
user.delete()
|
||||
@@ -180,10 +166,11 @@ class GetUpdateDeleteUser(APIView):
|
||||
|
||||
class UserActions(APIView):
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
|
||||
# reset password
|
||||
def post(self, request):
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
if _is_root_user(request, user):
|
||||
if is_root_user(request=request, user=user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.set_password(request.data["password"])
|
||||
@@ -194,7 +181,7 @@ class UserActions(APIView):
|
||||
# reset two factor token
|
||||
def put(self, request):
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
if _is_root_user(request, user):
|
||||
if is_root_user(request=request, user=user):
|
||||
return notify_error("The root user cannot be modified from the UI")
|
||||
|
||||
user.totp_key = ""
|
||||
@@ -206,10 +193,8 @@ class UserActions(APIView):
|
||||
|
||||
|
||||
class TOTPSetup(APIView):
|
||||
|
||||
# totp setup
|
||||
def post(self, request):
|
||||
|
||||
user = request.user
|
||||
if not user.totp_key:
|
||||
code = pyotp.random_base32()
|
||||
@@ -278,7 +263,7 @@ class GetAddAPIKeys(APIView):
|
||||
request.data["key"] = get_random_string(length=32).upper()
|
||||
serializer = APIKeySerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
serializer.save()
|
||||
return Response("The API Key was added")
|
||||
|
||||
|
||||
@@ -301,3 +286,23 @@ class GetUpdateDeleteAPIKey(APIView):
|
||||
apikey = get_object_or_404(APIKey, pk=pk)
|
||||
apikey.delete()
|
||||
return Response("The API Key was deleted")
|
||||
|
||||
|
||||
class ResetPass(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request):
|
||||
user = request.user
|
||||
user.set_password(request.data["password"])
|
||||
user.save()
|
||||
return Response("Password was reset.")
|
||||
|
||||
|
||||
class Reset2FA(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request):
|
||||
user = request.user
|
||||
user.totp_key = ""
|
||||
user.save()
|
||||
return Response("2FA was reset. Log out and back in to setup.")
|
||||
|
||||
@@ -9,7 +9,6 @@ from tacticalrmm.permissions import _has_perm_on_agent
|
||||
|
||||
class SendCMD(AsyncJsonWebsocketConsumer):
|
||||
async def connect(self):
|
||||
|
||||
self.user = self.scope["user"]
|
||||
|
||||
if isinstance(self.user, AnonymousUser):
|
||||
@@ -48,7 +47,7 @@ class SendCMD(AsyncJsonWebsocketConsumer):
|
||||
await self.send_json({"ret": ret})
|
||||
|
||||
async def disconnect(self, _):
|
||||
await self.close()
|
||||
pass
|
||||
|
||||
def _has_perm(self, perm: str) -> bool:
|
||||
if self.user.is_superuser or (
|
||||
|
||||
@@ -10,7 +10,7 @@ from tacticalrmm.utils import reload_nats
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Delete old agents"
|
||||
help = "Delete multiple agents based on criteria"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
@@ -23,6 +23,21 @@ class Command(BaseCommand):
|
||||
type=str,
|
||||
help="Delete agents that equal to or less than this version",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--site",
|
||||
type=str,
|
||||
help="Delete agents that belong to the specified site",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--client",
|
||||
type=str,
|
||||
help="Delete agents that belong to the specified client",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hostname",
|
||||
type=str,
|
||||
help="Delete agents with hostname starting with argument",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delete",
|
||||
action="store_true",
|
||||
@@ -32,25 +47,40 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
days = kwargs["days"]
|
||||
agentver = kwargs["agentver"]
|
||||
site = kwargs["site"]
|
||||
client = kwargs["client"]
|
||||
hostname = kwargs["hostname"]
|
||||
delete = kwargs["delete"]
|
||||
|
||||
if not days and not agentver:
|
||||
if not days and not agentver and not site and not client and not hostname:
|
||||
self.stdout.write(
|
||||
self.style.ERROR("Must have at least one parameter: days or agentver")
|
||||
self.style.ERROR(
|
||||
"Must have at least one parameter: days, agentver, site, client or hostname"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
q = Agent.objects.defer(*AGENT_DEFER)
|
||||
agents = Agent.objects.select_related("site__client").defer(*AGENT_DEFER)
|
||||
|
||||
agents = []
|
||||
if days:
|
||||
overdue = djangotime.now() - djangotime.timedelta(days=days)
|
||||
agents = [i for i in q if i.last_seen < overdue]
|
||||
agents = agents.filter(last_seen__lt=overdue)
|
||||
|
||||
if site:
|
||||
agents = agents.filter(site__name=site)
|
||||
|
||||
if client:
|
||||
agents = agents.filter(site__client__name=client)
|
||||
|
||||
if hostname:
|
||||
agents = agents.filter(hostname__istartswith=hostname)
|
||||
|
||||
if agentver:
|
||||
agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)]
|
||||
agents = [
|
||||
i for i in agents if pyver.parse(i.version) <= pyver.parse(agentver)
|
||||
]
|
||||
|
||||
if not agents:
|
||||
if len(agents) == 0:
|
||||
self.stdout.write(self.style.ERROR("No agents matched"))
|
||||
return
|
||||
|
||||
@@ -64,7 +94,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
agent.delete()
|
||||
except Exception as e:
|
||||
err = f"Failed to delete agent {agent.hostname}: {str(e)}"
|
||||
err = f"Failed to delete agent {agent.hostname}: {e}"
|
||||
self.stdout.write(self.style.ERROR(err))
|
||||
else:
|
||||
deleted_count += 1
|
||||
|
||||
@@ -5,14 +5,13 @@ from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from agents.models import Agent
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
from core.tasks import cache_db_fields_task
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "stuff for demo site in cron"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
random_dates = []
|
||||
now = djangotime.now()
|
||||
|
||||
@@ -30,4 +29,3 @@ class Command(BaseCommand):
|
||||
agent.save(update_fields=["last_seen"])
|
||||
|
||||
cache_db_fields_task()
|
||||
handle_resolved_stuff()
|
||||
|
||||
@@ -27,6 +27,7 @@ from tacticalrmm.constants import (
|
||||
EvtLogFailWhen,
|
||||
EvtLogNames,
|
||||
EvtLogTypes,
|
||||
GoArch,
|
||||
PAAction,
|
||||
ScriptShell,
|
||||
TaskSyncStatus,
|
||||
@@ -47,10 +48,12 @@ from tacticalrmm.demo_data import (
|
||||
temp_dir_stdout,
|
||||
wmi_deb,
|
||||
wmi_pi,
|
||||
wmi_mac,
|
||||
disks_mac,
|
||||
)
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
|
||||
AGENTS_TO_GENERATE = 20
|
||||
AGENTS_TO_GENERATE = 250
|
||||
|
||||
SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
|
||||
WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json")
|
||||
@@ -72,7 +75,6 @@ class Command(BaseCommand):
|
||||
return "".join(random.choice(chars) for _ in range(length))
|
||||
|
||||
def handle(self, *args, **kwargs) -> None:
|
||||
|
||||
user = User.objects.first()
|
||||
if user:
|
||||
user.totp_key = "ABSA234234"
|
||||
@@ -177,6 +179,8 @@ class Command(BaseCommand):
|
||||
"WSUS",
|
||||
"DESKTOP-12345",
|
||||
"LAPTOP-55443",
|
||||
"db-aws-01",
|
||||
"Karens-MacBook-Air.local",
|
||||
)
|
||||
descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
|
||||
modes = AgentMonType.values
|
||||
@@ -194,6 +198,7 @@ class Command(BaseCommand):
|
||||
|
||||
linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64"
|
||||
linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
|
||||
mac_os = "Darwin 12.5.1 arm64 21.6.0"
|
||||
|
||||
public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
|
||||
|
||||
@@ -289,7 +294,6 @@ class Command(BaseCommand):
|
||||
show_tmp_dir_script.save()
|
||||
|
||||
for count_agents in range(AGENTS_TO_GENERATE):
|
||||
|
||||
client = random.choice(clients)
|
||||
|
||||
if client == clients[0]:
|
||||
@@ -313,18 +317,25 @@ class Command(BaseCommand):
|
||||
mode = AgentMonType.SERVER
|
||||
# pi arm
|
||||
if plat_pick == 7:
|
||||
agent.goarch = "arm"
|
||||
agent.goarch = GoArch.ARM32
|
||||
agent.wmi_detail = wmi_pi
|
||||
agent.disks = disks_linux_pi
|
||||
agent.operating_system = linux_pi_os
|
||||
else:
|
||||
agent.goarch = "amd64"
|
||||
agent.goarch = GoArch.AMD64
|
||||
agent.wmi_detail = wmi_deb
|
||||
agent.disks = disks_linux_deb
|
||||
agent.operating_system = linux_deb_os
|
||||
elif plat_pick in (4, 14):
|
||||
agent.plat = AgentPlat.DARWIN
|
||||
mode = random.choice([AgentMonType.SERVER, AgentMonType.WORKSTATION])
|
||||
agent.goarch = GoArch.ARM64
|
||||
agent.wmi_detail = wmi_mac
|
||||
agent.disks = disks_mac
|
||||
agent.operating_system = mac_os
|
||||
else:
|
||||
agent.plat = AgentPlat.WINDOWS
|
||||
agent.goarch = "amd64"
|
||||
agent.goarch = GoArch.AMD64
|
||||
mode = random.choice(modes)
|
||||
agent.wmi_detail = random.choice(wmi_details)
|
||||
agent.services = services
|
||||
@@ -334,8 +345,8 @@ class Command(BaseCommand):
|
||||
else:
|
||||
agent.operating_system = random.choice(op_systems_workstations)
|
||||
|
||||
agent.hostname = random.choice(hostnames)
|
||||
agent.version = settings.LATEST_AGENT_VER
|
||||
agent.hostname = random.choice(hostnames)
|
||||
agent.site = Site.objects.get(name=site)
|
||||
agent.agent_id = self.rand_string(40)
|
||||
agent.description = random.choice(descriptions)
|
||||
@@ -568,6 +579,12 @@ class Command(BaseCommand):
|
||||
check5_history.y = 1
|
||||
else:
|
||||
check5_history.y = 0
|
||||
check5_history.results = {
|
||||
"retcode": 0,
|
||||
"stdout": None,
|
||||
"stderr": None,
|
||||
"execution_time": "4.0000",
|
||||
}
|
||||
check5_history.save()
|
||||
|
||||
check6 = Check()
|
||||
@@ -595,6 +612,12 @@ class Command(BaseCommand):
|
||||
check6_history.agent_id = agent.agent_id
|
||||
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check6_history.y = 0
|
||||
check6_history.results = {
|
||||
"retcode": 0,
|
||||
"stdout": None,
|
||||
"stderr": None,
|
||||
"execution_time": "4.0000",
|
||||
}
|
||||
check6_history.save()
|
||||
|
||||
nla_task = AutomatedTask()
|
||||
@@ -712,6 +735,12 @@ class Command(BaseCommand):
|
||||
check7_history.agent_id = agent.agent_id
|
||||
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check7_history.y = 0
|
||||
check7_history.results = {
|
||||
"retcode": 0,
|
||||
"stdout": spooler_stdout,
|
||||
"stderr": None,
|
||||
"execution_time": "3.1337",
|
||||
}
|
||||
check7_history.save()
|
||||
|
||||
if agent.plat == AgentPlat.WINDOWS:
|
||||
@@ -792,7 +821,6 @@ class Command(BaseCommand):
|
||||
pick = random.randint(1, 10)
|
||||
|
||||
if pick == 5 or pick == 3:
|
||||
|
||||
reboot_time = django_now + djangotime.timedelta(
|
||||
minutes=random.randint(1000, 500000)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.constants import AGENT_DEFER
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def find_duplicates(self, lst):
|
||||
return list(set([item for item in lst if lst.count(item) > 1]))
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
for agent in Agent.objects.defer(*AGENT_DEFER).prefetch_related(
|
||||
"custom_fields__field"
|
||||
):
|
||||
if dupes := self.find_duplicates(
|
||||
[i.field.name for i in agent.custom_fields.all()]
|
||||
):
|
||||
for dupe in dupes:
|
||||
cf = list(
|
||||
agent.custom_fields.filter(field__name=dupe).order_by("id")
|
||||
)
|
||||
to_delete = cf[:-1]
|
||||
for i in to_delete:
|
||||
i.delete()
|
||||
631
api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py
Normal file
631
api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# Generated by Django 4.1 on 2022-08-24 07:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("agents", "0054_alter_agent_goarch"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="agent",
|
||||
name="time_zone",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("Africa/Abidjan", "Africa/Abidjan"),
|
||||
("Africa/Accra", "Africa/Accra"),
|
||||
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||
("Africa/Algiers", "Africa/Algiers"),
|
||||
("Africa/Asmara", "Africa/Asmara"),
|
||||
("Africa/Asmera", "Africa/Asmera"),
|
||||
("Africa/Bamako", "Africa/Bamako"),
|
||||
("Africa/Bangui", "Africa/Bangui"),
|
||||
("Africa/Banjul", "Africa/Banjul"),
|
||||
("Africa/Bissau", "Africa/Bissau"),
|
||||
("Africa/Blantyre", "Africa/Blantyre"),
|
||||
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||
("Africa/Cairo", "Africa/Cairo"),
|
||||
("Africa/Casablanca", "Africa/Casablanca"),
|
||||
("Africa/Ceuta", "Africa/Ceuta"),
|
||||
("Africa/Conakry", "Africa/Conakry"),
|
||||
("Africa/Dakar", "Africa/Dakar"),
|
||||
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||
("Africa/Djibouti", "Africa/Djibouti"),
|
||||
("Africa/Douala", "Africa/Douala"),
|
||||
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||
("Africa/Freetown", "Africa/Freetown"),
|
||||
("Africa/Gaborone", "Africa/Gaborone"),
|
||||
("Africa/Harare", "Africa/Harare"),
|
||||
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||
("Africa/Juba", "Africa/Juba"),
|
||||
("Africa/Kampala", "Africa/Kampala"),
|
||||
("Africa/Khartoum", "Africa/Khartoum"),
|
||||
("Africa/Kigali", "Africa/Kigali"),
|
||||
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||
("Africa/Lagos", "Africa/Lagos"),
|
||||
("Africa/Libreville", "Africa/Libreville"),
|
||||
("Africa/Lome", "Africa/Lome"),
|
||||
("Africa/Luanda", "Africa/Luanda"),
|
||||
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||
("Africa/Lusaka", "Africa/Lusaka"),
|
||||
("Africa/Malabo", "Africa/Malabo"),
|
||||
("Africa/Maputo", "Africa/Maputo"),
|
||||
("Africa/Maseru", "Africa/Maseru"),
|
||||
("Africa/Mbabane", "Africa/Mbabane"),
|
||||
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||
("Africa/Monrovia", "Africa/Monrovia"),
|
||||
("Africa/Nairobi", "Africa/Nairobi"),
|
||||
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||
("Africa/Niamey", "Africa/Niamey"),
|
||||
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||
("Africa/Timbuktu", "Africa/Timbuktu"),
|
||||
("Africa/Tripoli", "Africa/Tripoli"),
|
||||
("Africa/Tunis", "Africa/Tunis"),
|
||||
("Africa/Windhoek", "Africa/Windhoek"),
|
||||
("America/Adak", "America/Adak"),
|
||||
("America/Anchorage", "America/Anchorage"),
|
||||
("America/Anguilla", "America/Anguilla"),
|
||||
("America/Antigua", "America/Antigua"),
|
||||
("America/Araguaina", "America/Araguaina"),
|
||||
(
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
),
|
||||
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
||||
(
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
),
|
||||
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||
(
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
),
|
||||
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||
("America/Aruba", "America/Aruba"),
|
||||
("America/Asuncion", "America/Asuncion"),
|
||||
("America/Atikokan", "America/Atikokan"),
|
||||
("America/Atka", "America/Atka"),
|
||||
("America/Bahia", "America/Bahia"),
|
||||
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||
("America/Barbados", "America/Barbados"),
|
||||
("America/Belem", "America/Belem"),
|
||||
("America/Belize", "America/Belize"),
|
||||
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||
("America/Boa_Vista", "America/Boa_Vista"),
|
||||
("America/Bogota", "America/Bogota"),
|
||||
("America/Boise", "America/Boise"),
|
||||
("America/Buenos_Aires", "America/Buenos_Aires"),
|
||||
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||
("America/Campo_Grande", "America/Campo_Grande"),
|
||||
("America/Cancun", "America/Cancun"),
|
||||
("America/Caracas", "America/Caracas"),
|
||||
("America/Catamarca", "America/Catamarca"),
|
||||
("America/Cayenne", "America/Cayenne"),
|
||||
("America/Cayman", "America/Cayman"),
|
||||
("America/Chicago", "America/Chicago"),
|
||||
("America/Chihuahua", "America/Chihuahua"),
|
||||
("America/Coral_Harbour", "America/Coral_Harbour"),
|
||||
("America/Cordoba", "America/Cordoba"),
|
||||
("America/Costa_Rica", "America/Costa_Rica"),
|
||||
("America/Creston", "America/Creston"),
|
||||
("America/Cuiaba", "America/Cuiaba"),
|
||||
("America/Curacao", "America/Curacao"),
|
||||
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||
("America/Dawson", "America/Dawson"),
|
||||
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||
("America/Denver", "America/Denver"),
|
||||
("America/Detroit", "America/Detroit"),
|
||||
("America/Dominica", "America/Dominica"),
|
||||
("America/Edmonton", "America/Edmonton"),
|
||||
("America/Eirunepe", "America/Eirunepe"),
|
||||
("America/El_Salvador", "America/El_Salvador"),
|
||||
("America/Ensenada", "America/Ensenada"),
|
||||
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||
("America/Fort_Wayne", "America/Fort_Wayne"),
|
||||
("America/Fortaleza", "America/Fortaleza"),
|
||||
("America/Glace_Bay", "America/Glace_Bay"),
|
||||
("America/Godthab", "America/Godthab"),
|
||||
("America/Goose_Bay", "America/Goose_Bay"),
|
||||
("America/Grand_Turk", "America/Grand_Turk"),
|
||||
("America/Grenada", "America/Grenada"),
|
||||
("America/Guadeloupe", "America/Guadeloupe"),
|
||||
("America/Guatemala", "America/Guatemala"),
|
||||
("America/Guayaquil", "America/Guayaquil"),
|
||||
("America/Guyana", "America/Guyana"),
|
||||
("America/Halifax", "America/Halifax"),
|
||||
("America/Havana", "America/Havana"),
|
||||
("America/Hermosillo", "America/Hermosillo"),
|
||||
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||
("America/Indianapolis", "America/Indianapolis"),
|
||||
("America/Inuvik", "America/Inuvik"),
|
||||
("America/Iqaluit", "America/Iqaluit"),
|
||||
("America/Jamaica", "America/Jamaica"),
|
||||
("America/Jujuy", "America/Jujuy"),
|
||||
("America/Juneau", "America/Juneau"),
|
||||
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
||||
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
||||
("America/Knox_IN", "America/Knox_IN"),
|
||||
("America/Kralendijk", "America/Kralendijk"),
|
||||
("America/La_Paz", "America/La_Paz"),
|
||||
("America/Lima", "America/Lima"),
|
||||
("America/Los_Angeles", "America/Los_Angeles"),
|
||||
("America/Louisville", "America/Louisville"),
|
||||
("America/Lower_Princes", "America/Lower_Princes"),
|
||||
("America/Maceio", "America/Maceio"),
|
||||
("America/Managua", "America/Managua"),
|
||||
("America/Manaus", "America/Manaus"),
|
||||
("America/Marigot", "America/Marigot"),
|
||||
("America/Martinique", "America/Martinique"),
|
||||
("America/Matamoros", "America/Matamoros"),
|
||||
("America/Mazatlan", "America/Mazatlan"),
|
||||
("America/Mendoza", "America/Mendoza"),
|
||||
("America/Menominee", "America/Menominee"),
|
||||
("America/Merida", "America/Merida"),
|
||||
("America/Metlakatla", "America/Metlakatla"),
|
||||
("America/Mexico_City", "America/Mexico_City"),
|
||||
("America/Miquelon", "America/Miquelon"),
|
||||
("America/Moncton", "America/Moncton"),
|
||||
("America/Monterrey", "America/Monterrey"),
|
||||
("America/Montevideo", "America/Montevideo"),
|
||||
("America/Montreal", "America/Montreal"),
|
||||
("America/Montserrat", "America/Montserrat"),
|
||||
("America/Nassau", "America/Nassau"),
|
||||
("America/New_York", "America/New_York"),
|
||||
("America/Nipigon", "America/Nipigon"),
|
||||
("America/Nome", "America/Nome"),
|
||||
("America/Noronha", "America/Noronha"),
|
||||
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
||||
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
||||
(
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/North_Dakota/New_Salem",
|
||||
),
|
||||
("America/Nuuk", "America/Nuuk"),
|
||||
("America/Ojinaga", "America/Ojinaga"),
|
||||
("America/Panama", "America/Panama"),
|
||||
("America/Pangnirtung", "America/Pangnirtung"),
|
||||
("America/Paramaribo", "America/Paramaribo"),
|
||||
("America/Phoenix", "America/Phoenix"),
|
||||
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||
("America/Porto_Acre", "America/Porto_Acre"),
|
||||
("America/Porto_Velho", "America/Porto_Velho"),
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||
("America/Rainy_River", "America/Rainy_River"),
|
||||
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||
("America/Recife", "America/Recife"),
|
||||
("America/Regina", "America/Regina"),
|
||||
("America/Resolute", "America/Resolute"),
|
||||
("America/Rio_Branco", "America/Rio_Branco"),
|
||||
("America/Rosario", "America/Rosario"),
|
||||
("America/Santa_Isabel", "America/Santa_Isabel"),
|
||||
("America/Santarem", "America/Santarem"),
|
||||
("America/Santiago", "America/Santiago"),
|
||||
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||
("America/Scoresbysund", "America/Scoresbysund"),
|
||||
("America/Shiprock", "America/Shiprock"),
|
||||
("America/Sitka", "America/Sitka"),
|
||||
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||
("America/St_Johns", "America/St_Johns"),
|
||||
("America/St_Kitts", "America/St_Kitts"),
|
||||
("America/St_Lucia", "America/St_Lucia"),
|
||||
("America/St_Thomas", "America/St_Thomas"),
|
||||
("America/St_Vincent", "America/St_Vincent"),
|
||||
("America/Swift_Current", "America/Swift_Current"),
|
||||
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||
("America/Thule", "America/Thule"),
|
||||
("America/Thunder_Bay", "America/Thunder_Bay"),
|
||||
("America/Tijuana", "America/Tijuana"),
|
||||
("America/Toronto", "America/Toronto"),
|
||||
("America/Tortola", "America/Tortola"),
|
||||
("America/Vancouver", "America/Vancouver"),
|
||||
("America/Virgin", "America/Virgin"),
|
||||
("America/Whitehorse", "America/Whitehorse"),
|
||||
("America/Winnipeg", "America/Winnipeg"),
|
||||
("America/Yakutat", "America/Yakutat"),
|
||||
("America/Yellowknife", "America/Yellowknife"),
|
||||
("Antarctica/Casey", "Antarctica/Casey"),
|
||||
("Antarctica/Davis", "Antarctica/Davis"),
|
||||
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||
("Antarctica/South_Pole", "Antarctica/South_Pole"),
|
||||
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||
("Antarctica/Troll", "Antarctica/Troll"),
|
||||
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||
("Asia/Aden", "Asia/Aden"),
|
||||
("Asia/Almaty", "Asia/Almaty"),
|
||||
("Asia/Amman", "Asia/Amman"),
|
||||
("Asia/Anadyr", "Asia/Anadyr"),
|
||||
("Asia/Aqtau", "Asia/Aqtau"),
|
||||
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||
("Asia/Ashkhabad", "Asia/Ashkhabad"),
|
||||
("Asia/Atyrau", "Asia/Atyrau"),
|
||||
("Asia/Baghdad", "Asia/Baghdad"),
|
||||
("Asia/Bahrain", "Asia/Bahrain"),
|
||||
("Asia/Baku", "Asia/Baku"),
|
||||
("Asia/Bangkok", "Asia/Bangkok"),
|
||||
("Asia/Barnaul", "Asia/Barnaul"),
|
||||
("Asia/Beirut", "Asia/Beirut"),
|
||||
("Asia/Bishkek", "Asia/Bishkek"),
|
||||
("Asia/Brunei", "Asia/Brunei"),
|
||||
("Asia/Calcutta", "Asia/Calcutta"),
|
||||
("Asia/Chita", "Asia/Chita"),
|
||||
("Asia/Choibalsan", "Asia/Choibalsan"),
|
||||
("Asia/Chongqing", "Asia/Chongqing"),
|
||||
("Asia/Chungking", "Asia/Chungking"),
|
||||
("Asia/Colombo", "Asia/Colombo"),
|
||||
("Asia/Dacca", "Asia/Dacca"),
|
||||
("Asia/Damascus", "Asia/Damascus"),
|
||||
("Asia/Dhaka", "Asia/Dhaka"),
|
||||
("Asia/Dili", "Asia/Dili"),
|
||||
("Asia/Dubai", "Asia/Dubai"),
|
||||
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||
("Asia/Famagusta", "Asia/Famagusta"),
|
||||
("Asia/Gaza", "Asia/Gaza"),
|
||||
("Asia/Harbin", "Asia/Harbin"),
|
||||
("Asia/Hebron", "Asia/Hebron"),
|
||||
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||
("Asia/Hovd", "Asia/Hovd"),
|
||||
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||
("Asia/Istanbul", "Asia/Istanbul"),
|
||||
("Asia/Jakarta", "Asia/Jakarta"),
|
||||
("Asia/Jayapura", "Asia/Jayapura"),
|
||||
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||
("Asia/Kabul", "Asia/Kabul"),
|
||||
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||
("Asia/Karachi", "Asia/Karachi"),
|
||||
("Asia/Kashgar", "Asia/Kashgar"),
|
||||
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||
("Asia/Katmandu", "Asia/Katmandu"),
|
||||
("Asia/Khandyga", "Asia/Khandyga"),
|
||||
("Asia/Kolkata", "Asia/Kolkata"),
|
||||
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||
("Asia/Kuching", "Asia/Kuching"),
|
||||
("Asia/Kuwait", "Asia/Kuwait"),
|
||||
("Asia/Macao", "Asia/Macao"),
|
||||
("Asia/Macau", "Asia/Macau"),
|
||||
("Asia/Magadan", "Asia/Magadan"),
|
||||
("Asia/Makassar", "Asia/Makassar"),
|
||||
("Asia/Manila", "Asia/Manila"),
|
||||
("Asia/Muscat", "Asia/Muscat"),
|
||||
("Asia/Nicosia", "Asia/Nicosia"),
|
||||
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||
("Asia/Omsk", "Asia/Omsk"),
|
||||
("Asia/Oral", "Asia/Oral"),
|
||||
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||
("Asia/Pontianak", "Asia/Pontianak"),
|
||||
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||
("Asia/Qatar", "Asia/Qatar"),
|
||||
("Asia/Qostanay", "Asia/Qostanay"),
|
||||
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||
("Asia/Rangoon", "Asia/Rangoon"),
|
||||
("Asia/Riyadh", "Asia/Riyadh"),
|
||||
("Asia/Saigon", "Asia/Saigon"),
|
||||
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||
("Asia/Samarkand", "Asia/Samarkand"),
|
||||
("Asia/Seoul", "Asia/Seoul"),
|
||||
("Asia/Shanghai", "Asia/Shanghai"),
|
||||
("Asia/Singapore", "Asia/Singapore"),
|
||||
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||
("Asia/Taipei", "Asia/Taipei"),
|
||||
("Asia/Tashkent", "Asia/Tashkent"),
|
||||
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||
("Asia/Tehran", "Asia/Tehran"),
|
||||
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
|
||||
("Asia/Thimbu", "Asia/Thimbu"),
|
||||
("Asia/Thimphu", "Asia/Thimphu"),
|
||||
("Asia/Tokyo", "Asia/Tokyo"),
|
||||
("Asia/Tomsk", "Asia/Tomsk"),
|
||||
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
|
||||
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
|
||||
("Asia/Urumqi", "Asia/Urumqi"),
|
||||
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||
("Asia/Vientiane", "Asia/Vientiane"),
|
||||
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||
("Asia/Yangon", "Asia/Yangon"),
|
||||
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||
("Asia/Yerevan", "Asia/Yerevan"),
|
||||
("Atlantic/Azores", "Atlantic/Azores"),
|
||||
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||
("Atlantic/Canary", "Atlantic/Canary"),
|
||||
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||
("Atlantic/Faeroe", "Atlantic/Faeroe"),
|
||||
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
|
||||
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||
("Australia/ACT", "Australia/ACT"),
|
||||
("Australia/Adelaide", "Australia/Adelaide"),
|
||||
("Australia/Brisbane", "Australia/Brisbane"),
|
||||
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||
("Australia/Canberra", "Australia/Canberra"),
|
||||
("Australia/Currie", "Australia/Currie"),
|
||||
("Australia/Darwin", "Australia/Darwin"),
|
||||
("Australia/Eucla", "Australia/Eucla"),
|
||||
("Australia/Hobart", "Australia/Hobart"),
|
||||
("Australia/LHI", "Australia/LHI"),
|
||||
("Australia/Lindeman", "Australia/Lindeman"),
|
||||
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||
("Australia/Melbourne", "Australia/Melbourne"),
|
||||
("Australia/NSW", "Australia/NSW"),
|
||||
("Australia/North", "Australia/North"),
|
||||
("Australia/Perth", "Australia/Perth"),
|
||||
("Australia/Queensland", "Australia/Queensland"),
|
||||
("Australia/South", "Australia/South"),
|
||||
("Australia/Sydney", "Australia/Sydney"),
|
||||
("Australia/Tasmania", "Australia/Tasmania"),
|
||||
("Australia/Victoria", "Australia/Victoria"),
|
||||
("Australia/West", "Australia/West"),
|
||||
("Australia/Yancowinna", "Australia/Yancowinna"),
|
||||
("Brazil/Acre", "Brazil/Acre"),
|
||||
("Brazil/DeNoronha", "Brazil/DeNoronha"),
|
||||
("Brazil/East", "Brazil/East"),
|
||||
("Brazil/West", "Brazil/West"),
|
||||
("CET", "CET"),
|
||||
("CST6CDT", "CST6CDT"),
|
||||
("Canada/Atlantic", "Canada/Atlantic"),
|
||||
("Canada/Central", "Canada/Central"),
|
||||
("Canada/Eastern", "Canada/Eastern"),
|
||||
("Canada/Mountain", "Canada/Mountain"),
|
||||
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||
("Canada/Pacific", "Canada/Pacific"),
|
||||
("Canada/Saskatchewan", "Canada/Saskatchewan"),
|
||||
("Canada/Yukon", "Canada/Yukon"),
|
||||
("Chile/Continental", "Chile/Continental"),
|
||||
("Chile/EasterIsland", "Chile/EasterIsland"),
|
||||
("Cuba", "Cuba"),
|
||||
("EET", "EET"),
|
||||
("EST", "EST"),
|
||||
("EST5EDT", "EST5EDT"),
|
||||
("Egypt", "Egypt"),
|
||||
("Eire", "Eire"),
|
||||
("Etc/GMT", "Etc/GMT"),
|
||||
("Etc/GMT+0", "Etc/GMT+0"),
|
||||
("Etc/GMT+1", "Etc/GMT+1"),
|
||||
("Etc/GMT+10", "Etc/GMT+10"),
|
||||
("Etc/GMT+11", "Etc/GMT+11"),
|
||||
("Etc/GMT+12", "Etc/GMT+12"),
|
||||
("Etc/GMT+2", "Etc/GMT+2"),
|
||||
("Etc/GMT+3", "Etc/GMT+3"),
|
||||
("Etc/GMT+4", "Etc/GMT+4"),
|
||||
("Etc/GMT+5", "Etc/GMT+5"),
|
||||
("Etc/GMT+6", "Etc/GMT+6"),
|
||||
("Etc/GMT+7", "Etc/GMT+7"),
|
||||
("Etc/GMT+8", "Etc/GMT+8"),
|
||||
("Etc/GMT+9", "Etc/GMT+9"),
|
||||
("Etc/GMT-0", "Etc/GMT-0"),
|
||||
("Etc/GMT-1", "Etc/GMT-1"),
|
||||
("Etc/GMT-10", "Etc/GMT-10"),
|
||||
("Etc/GMT-11", "Etc/GMT-11"),
|
||||
("Etc/GMT-12", "Etc/GMT-12"),
|
||||
("Etc/GMT-13", "Etc/GMT-13"),
|
||||
("Etc/GMT-14", "Etc/GMT-14"),
|
||||
("Etc/GMT-2", "Etc/GMT-2"),
|
||||
("Etc/GMT-3", "Etc/GMT-3"),
|
||||
("Etc/GMT-4", "Etc/GMT-4"),
|
||||
("Etc/GMT-5", "Etc/GMT-5"),
|
||||
("Etc/GMT-6", "Etc/GMT-6"),
|
||||
("Etc/GMT-7", "Etc/GMT-7"),
|
||||
("Etc/GMT-8", "Etc/GMT-8"),
|
||||
("Etc/GMT-9", "Etc/GMT-9"),
|
||||
("Etc/GMT0", "Etc/GMT0"),
|
||||
("Etc/Greenwich", "Etc/Greenwich"),
|
||||
("Etc/UCT", "Etc/UCT"),
|
||||
("Etc/UTC", "Etc/UTC"),
|
||||
("Etc/Universal", "Etc/Universal"),
|
||||
("Etc/Zulu", "Etc/Zulu"),
|
||||
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||
("Europe/Andorra", "Europe/Andorra"),
|
||||
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||
("Europe/Athens", "Europe/Athens"),
|
||||
("Europe/Belfast", "Europe/Belfast"),
|
||||
("Europe/Belgrade", "Europe/Belgrade"),
|
||||
("Europe/Berlin", "Europe/Berlin"),
|
||||
("Europe/Bratislava", "Europe/Bratislava"),
|
||||
("Europe/Brussels", "Europe/Brussels"),
|
||||
("Europe/Bucharest", "Europe/Bucharest"),
|
||||
("Europe/Budapest", "Europe/Budapest"),
|
||||
("Europe/Busingen", "Europe/Busingen"),
|
||||
("Europe/Chisinau", "Europe/Chisinau"),
|
||||
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||
("Europe/Dublin", "Europe/Dublin"),
|
||||
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||
("Europe/Guernsey", "Europe/Guernsey"),
|
||||
("Europe/Helsinki", "Europe/Helsinki"),
|
||||
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||
("Europe/Istanbul", "Europe/Istanbul"),
|
||||
("Europe/Jersey", "Europe/Jersey"),
|
||||
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||
("Europe/Kiev", "Europe/Kiev"),
|
||||
("Europe/Kirov", "Europe/Kirov"),
|
||||
("Europe/Kyiv", "Europe/Kyiv"),
|
||||
("Europe/Lisbon", "Europe/Lisbon"),
|
||||
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||
("Europe/London", "Europe/London"),
|
||||
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||
("Europe/Madrid", "Europe/Madrid"),
|
||||
("Europe/Malta", "Europe/Malta"),
|
||||
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||
("Europe/Minsk", "Europe/Minsk"),
|
||||
("Europe/Monaco", "Europe/Monaco"),
|
||||
("Europe/Moscow", "Europe/Moscow"),
|
||||
("Europe/Nicosia", "Europe/Nicosia"),
|
||||
("Europe/Oslo", "Europe/Oslo"),
|
||||
("Europe/Paris", "Europe/Paris"),
|
||||
("Europe/Podgorica", "Europe/Podgorica"),
|
||||
("Europe/Prague", "Europe/Prague"),
|
||||
("Europe/Riga", "Europe/Riga"),
|
||||
("Europe/Rome", "Europe/Rome"),
|
||||
("Europe/Samara", "Europe/Samara"),
|
||||
("Europe/San_Marino", "Europe/San_Marino"),
|
||||
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||
("Europe/Saratov", "Europe/Saratov"),
|
||||
("Europe/Simferopol", "Europe/Simferopol"),
|
||||
("Europe/Skopje", "Europe/Skopje"),
|
||||
("Europe/Sofia", "Europe/Sofia"),
|
||||
("Europe/Stockholm", "Europe/Stockholm"),
|
||||
("Europe/Tallinn", "Europe/Tallinn"),
|
||||
("Europe/Tirane", "Europe/Tirane"),
|
||||
("Europe/Tiraspol", "Europe/Tiraspol"),
|
||||
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||
("Europe/Uzhgorod", "Europe/Uzhgorod"),
|
||||
("Europe/Vaduz", "Europe/Vaduz"),
|
||||
("Europe/Vatican", "Europe/Vatican"),
|
||||
("Europe/Vienna", "Europe/Vienna"),
|
||||
("Europe/Vilnius", "Europe/Vilnius"),
|
||||
("Europe/Volgograd", "Europe/Volgograd"),
|
||||
("Europe/Warsaw", "Europe/Warsaw"),
|
||||
("Europe/Zagreb", "Europe/Zagreb"),
|
||||
("Europe/Zaporozhye", "Europe/Zaporozhye"),
|
||||
("Europe/Zurich", "Europe/Zurich"),
|
||||
("GB", "GB"),
|
||||
("GB-Eire", "GB-Eire"),
|
||||
("GMT", "GMT"),
|
||||
("GMT+0", "GMT+0"),
|
||||
("GMT-0", "GMT-0"),
|
||||
("GMT0", "GMT0"),
|
||||
("Greenwich", "Greenwich"),
|
||||
("HST", "HST"),
|
||||
("Hongkong", "Hongkong"),
|
||||
("Iceland", "Iceland"),
|
||||
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||
("Indian/Chagos", "Indian/Chagos"),
|
||||
("Indian/Christmas", "Indian/Christmas"),
|
||||
("Indian/Cocos", "Indian/Cocos"),
|
||||
("Indian/Comoro", "Indian/Comoro"),
|
||||
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||
("Indian/Mahe", "Indian/Mahe"),
|
||||
("Indian/Maldives", "Indian/Maldives"),
|
||||
("Indian/Mauritius", "Indian/Mauritius"),
|
||||
("Indian/Mayotte", "Indian/Mayotte"),
|
||||
("Indian/Reunion", "Indian/Reunion"),
|
||||
("Iran", "Iran"),
|
||||
("Israel", "Israel"),
|
||||
("Jamaica", "Jamaica"),
|
||||
("Japan", "Japan"),
|
||||
("Kwajalein", "Kwajalein"),
|
||||
("Libya", "Libya"),
|
||||
("MET", "MET"),
|
||||
("MST", "MST"),
|
||||
("MST7MDT", "MST7MDT"),
|
||||
("Mexico/BajaNorte", "Mexico/BajaNorte"),
|
||||
("Mexico/BajaSur", "Mexico/BajaSur"),
|
||||
("Mexico/General", "Mexico/General"),
|
||||
("NZ", "NZ"),
|
||||
("NZ-CHAT", "NZ-CHAT"),
|
||||
("Navajo", "Navajo"),
|
||||
("PRC", "PRC"),
|
||||
("PST8PDT", "PST8PDT"),
|
||||
("Pacific/Apia", "Pacific/Apia"),
|
||||
("Pacific/Auckland", "Pacific/Auckland"),
|
||||
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||
("Pacific/Chatham", "Pacific/Chatham"),
|
||||
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||
("Pacific/Easter", "Pacific/Easter"),
|
||||
("Pacific/Efate", "Pacific/Efate"),
|
||||
("Pacific/Enderbury", "Pacific/Enderbury"),
|
||||
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||
("Pacific/Fiji", "Pacific/Fiji"),
|
||||
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||
("Pacific/Gambier", "Pacific/Gambier"),
|
||||
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||
("Pacific/Guam", "Pacific/Guam"),
|
||||
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||
("Pacific/Johnston", "Pacific/Johnston"),
|
||||
("Pacific/Kanton", "Pacific/Kanton"),
|
||||
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||
("Pacific/Majuro", "Pacific/Majuro"),
|
||||
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||
("Pacific/Midway", "Pacific/Midway"),
|
||||
("Pacific/Nauru", "Pacific/Nauru"),
|
||||
("Pacific/Niue", "Pacific/Niue"),
|
||||
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||
("Pacific/Noumea", "Pacific/Noumea"),
|
||||
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||
("Pacific/Palau", "Pacific/Palau"),
|
||||
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||
("Pacific/Ponape", "Pacific/Ponape"),
|
||||
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||
("Pacific/Saipan", "Pacific/Saipan"),
|
||||
("Pacific/Samoa", "Pacific/Samoa"),
|
||||
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||
("Pacific/Truk", "Pacific/Truk"),
|
||||
("Pacific/Wake", "Pacific/Wake"),
|
||||
("Pacific/Wallis", "Pacific/Wallis"),
|
||||
("Pacific/Yap", "Pacific/Yap"),
|
||||
("Poland", "Poland"),
|
||||
("Portugal", "Portugal"),
|
||||
("ROC", "ROC"),
|
||||
("ROK", "ROK"),
|
||||
("Singapore", "Singapore"),
|
||||
("Turkey", "Turkey"),
|
||||
("UCT", "UCT"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("US/Arizona", "US/Arizona"),
|
||||
("US/Central", "US/Central"),
|
||||
("US/East-Indiana", "US/East-Indiana"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("US/Indiana-Starke", "US/Indiana-Starke"),
|
||||
("US/Michigan", "US/Michigan"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
("UTC", "UTC"),
|
||||
("Universal", "Universal"),
|
||||
("W-SU", "W-SU"),
|
||||
("WET", "WET"),
|
||||
("Zulu", "Zulu"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
631
api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
Normal file
631
api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-28 22:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("agents", "0055_alter_agent_time_zone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="agent",
|
||||
name="time_zone",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("Africa/Abidjan", "Africa/Abidjan"),
|
||||
("Africa/Accra", "Africa/Accra"),
|
||||
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||
("Africa/Algiers", "Africa/Algiers"),
|
||||
("Africa/Asmara", "Africa/Asmara"),
|
||||
("Africa/Asmera", "Africa/Asmera"),
|
||||
("Africa/Bamako", "Africa/Bamako"),
|
||||
("Africa/Bangui", "Africa/Bangui"),
|
||||
("Africa/Banjul", "Africa/Banjul"),
|
||||
("Africa/Bissau", "Africa/Bissau"),
|
||||
("Africa/Blantyre", "Africa/Blantyre"),
|
||||
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||
("Africa/Cairo", "Africa/Cairo"),
|
||||
("Africa/Casablanca", "Africa/Casablanca"),
|
||||
("Africa/Ceuta", "Africa/Ceuta"),
|
||||
("Africa/Conakry", "Africa/Conakry"),
|
||||
("Africa/Dakar", "Africa/Dakar"),
|
||||
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||
("Africa/Djibouti", "Africa/Djibouti"),
|
||||
("Africa/Douala", "Africa/Douala"),
|
||||
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||
("Africa/Freetown", "Africa/Freetown"),
|
||||
("Africa/Gaborone", "Africa/Gaborone"),
|
||||
("Africa/Harare", "Africa/Harare"),
|
||||
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||
("Africa/Juba", "Africa/Juba"),
|
||||
("Africa/Kampala", "Africa/Kampala"),
|
||||
("Africa/Khartoum", "Africa/Khartoum"),
|
||||
("Africa/Kigali", "Africa/Kigali"),
|
||||
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||
("Africa/Lagos", "Africa/Lagos"),
|
||||
("Africa/Libreville", "Africa/Libreville"),
|
||||
("Africa/Lome", "Africa/Lome"),
|
||||
("Africa/Luanda", "Africa/Luanda"),
|
||||
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||
("Africa/Lusaka", "Africa/Lusaka"),
|
||||
("Africa/Malabo", "Africa/Malabo"),
|
||||
("Africa/Maputo", "Africa/Maputo"),
|
||||
("Africa/Maseru", "Africa/Maseru"),
|
||||
("Africa/Mbabane", "Africa/Mbabane"),
|
||||
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||
("Africa/Monrovia", "Africa/Monrovia"),
|
||||
("Africa/Nairobi", "Africa/Nairobi"),
|
||||
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||
("Africa/Niamey", "Africa/Niamey"),
|
||||
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||
("Africa/Timbuktu", "Africa/Timbuktu"),
|
||||
("Africa/Tripoli", "Africa/Tripoli"),
|
||||
("Africa/Tunis", "Africa/Tunis"),
|
||||
("Africa/Windhoek", "Africa/Windhoek"),
|
||||
("America/Adak", "America/Adak"),
|
||||
("America/Anchorage", "America/Anchorage"),
|
||||
("America/Anguilla", "America/Anguilla"),
|
||||
("America/Antigua", "America/Antigua"),
|
||||
("America/Araguaina", "America/Araguaina"),
|
||||
(
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
),
|
||||
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
||||
(
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
),
|
||||
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||
(
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
),
|
||||
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||
("America/Aruba", "America/Aruba"),
|
||||
("America/Asuncion", "America/Asuncion"),
|
||||
("America/Atikokan", "America/Atikokan"),
|
||||
("America/Atka", "America/Atka"),
|
||||
("America/Bahia", "America/Bahia"),
|
||||
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||
("America/Barbados", "America/Barbados"),
|
||||
("America/Belem", "America/Belem"),
|
||||
("America/Belize", "America/Belize"),
|
||||
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||
("America/Boa_Vista", "America/Boa_Vista"),
|
||||
("America/Bogota", "America/Bogota"),
|
||||
("America/Boise", "America/Boise"),
|
||||
("America/Buenos_Aires", "America/Buenos_Aires"),
|
||||
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||
("America/Campo_Grande", "America/Campo_Grande"),
|
||||
("America/Cancun", "America/Cancun"),
|
||||
("America/Caracas", "America/Caracas"),
|
||||
("America/Catamarca", "America/Catamarca"),
|
||||
("America/Cayenne", "America/Cayenne"),
|
||||
("America/Cayman", "America/Cayman"),
|
||||
("America/Chicago", "America/Chicago"),
|
||||
("America/Chihuahua", "America/Chihuahua"),
|
||||
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("America/Coral_Harbour", "America/Coral_Harbour"),
|
||||
("America/Cordoba", "America/Cordoba"),
|
||||
("America/Costa_Rica", "America/Costa_Rica"),
|
||||
("America/Creston", "America/Creston"),
|
||||
("America/Cuiaba", "America/Cuiaba"),
|
||||
("America/Curacao", "America/Curacao"),
|
||||
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||
("America/Dawson", "America/Dawson"),
|
||||
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||
("America/Denver", "America/Denver"),
|
||||
("America/Detroit", "America/Detroit"),
|
||||
("America/Dominica", "America/Dominica"),
|
||||
("America/Edmonton", "America/Edmonton"),
|
||||
("America/Eirunepe", "America/Eirunepe"),
|
||||
("America/El_Salvador", "America/El_Salvador"),
|
||||
("America/Ensenada", "America/Ensenada"),
|
||||
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||
("America/Fort_Wayne", "America/Fort_Wayne"),
|
||||
("America/Fortaleza", "America/Fortaleza"),
|
||||
("America/Glace_Bay", "America/Glace_Bay"),
|
||||
("America/Godthab", "America/Godthab"),
|
||||
("America/Goose_Bay", "America/Goose_Bay"),
|
||||
("America/Grand_Turk", "America/Grand_Turk"),
|
||||
("America/Grenada", "America/Grenada"),
|
||||
("America/Guadeloupe", "America/Guadeloupe"),
|
||||
("America/Guatemala", "America/Guatemala"),
|
||||
("America/Guayaquil", "America/Guayaquil"),
|
||||
("America/Guyana", "America/Guyana"),
|
||||
("America/Halifax", "America/Halifax"),
|
||||
("America/Havana", "America/Havana"),
|
||||
("America/Hermosillo", "America/Hermosillo"),
|
||||
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||
("America/Indianapolis", "America/Indianapolis"),
|
||||
("America/Inuvik", "America/Inuvik"),
|
||||
("America/Iqaluit", "America/Iqaluit"),
|
||||
("America/Jamaica", "America/Jamaica"),
|
||||
("America/Jujuy", "America/Jujuy"),
|
||||
("America/Juneau", "America/Juneau"),
|
||||
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
||||
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
||||
("America/Knox_IN", "America/Knox_IN"),
|
||||
("America/Kralendijk", "America/Kralendijk"),
|
||||
("America/La_Paz", "America/La_Paz"),
|
||||
("America/Lima", "America/Lima"),
|
||||
("America/Los_Angeles", "America/Los_Angeles"),
|
||||
("America/Louisville", "America/Louisville"),
|
||||
("America/Lower_Princes", "America/Lower_Princes"),
|
||||
("America/Maceio", "America/Maceio"),
|
||||
("America/Managua", "America/Managua"),
|
||||
("America/Manaus", "America/Manaus"),
|
||||
("America/Marigot", "America/Marigot"),
|
||||
("America/Martinique", "America/Martinique"),
|
||||
("America/Matamoros", "America/Matamoros"),
|
||||
("America/Mazatlan", "America/Mazatlan"),
|
||||
("America/Mendoza", "America/Mendoza"),
|
||||
("America/Menominee", "America/Menominee"),
|
||||
("America/Merida", "America/Merida"),
|
||||
("America/Metlakatla", "America/Metlakatla"),
|
||||
("America/Mexico_City", "America/Mexico_City"),
|
||||
("America/Miquelon", "America/Miquelon"),
|
||||
("America/Moncton", "America/Moncton"),
|
||||
("America/Monterrey", "America/Monterrey"),
|
||||
("America/Montevideo", "America/Montevideo"),
|
||||
("America/Montreal", "America/Montreal"),
|
||||
("America/Montserrat", "America/Montserrat"),
|
||||
("America/Nassau", "America/Nassau"),
|
||||
("America/New_York", "America/New_York"),
|
||||
("America/Nipigon", "America/Nipigon"),
|
||||
("America/Nome", "America/Nome"),
|
||||
("America/Noronha", "America/Noronha"),
|
||||
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
||||
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
||||
(
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/North_Dakota/New_Salem",
|
||||
),
|
||||
("America/Nuuk", "America/Nuuk"),
|
||||
("America/Ojinaga", "America/Ojinaga"),
|
||||
("America/Panama", "America/Panama"),
|
||||
("America/Pangnirtung", "America/Pangnirtung"),
|
||||
("America/Paramaribo", "America/Paramaribo"),
|
||||
("America/Phoenix", "America/Phoenix"),
|
||||
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||
("America/Porto_Acre", "America/Porto_Acre"),
|
||||
("America/Porto_Velho", "America/Porto_Velho"),
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||
("America/Rainy_River", "America/Rainy_River"),
|
||||
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||
("America/Recife", "America/Recife"),
|
||||
("America/Regina", "America/Regina"),
|
||||
("America/Resolute", "America/Resolute"),
|
||||
("America/Rio_Branco", "America/Rio_Branco"),
|
||||
("America/Rosario", "America/Rosario"),
|
||||
("America/Santa_Isabel", "America/Santa_Isabel"),
|
||||
("America/Santarem", "America/Santarem"),
|
||||
("America/Santiago", "America/Santiago"),
|
||||
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||
("America/Scoresbysund", "America/Scoresbysund"),
|
||||
("America/Shiprock", "America/Shiprock"),
|
||||
("America/Sitka", "America/Sitka"),
|
||||
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||
("America/St_Johns", "America/St_Johns"),
|
||||
("America/St_Kitts", "America/St_Kitts"),
|
||||
("America/St_Lucia", "America/St_Lucia"),
|
||||
("America/St_Thomas", "America/St_Thomas"),
|
||||
("America/St_Vincent", "America/St_Vincent"),
|
||||
("America/Swift_Current", "America/Swift_Current"),
|
||||
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||
("America/Thule", "America/Thule"),
|
||||
("America/Thunder_Bay", "America/Thunder_Bay"),
|
||||
("America/Tijuana", "America/Tijuana"),
|
||||
("America/Toronto", "America/Toronto"),
|
||||
("America/Tortola", "America/Tortola"),
|
||||
("America/Vancouver", "America/Vancouver"),
|
||||
("America/Virgin", "America/Virgin"),
|
||||
("America/Whitehorse", "America/Whitehorse"),
|
||||
("America/Winnipeg", "America/Winnipeg"),
|
||||
("America/Yakutat", "America/Yakutat"),
|
||||
("America/Yellowknife", "America/Yellowknife"),
|
||||
("Antarctica/Casey", "Antarctica/Casey"),
|
||||
("Antarctica/Davis", "Antarctica/Davis"),
|
||||
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||
("Antarctica/South_Pole", "Antarctica/South_Pole"),
|
||||
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||
("Antarctica/Troll", "Antarctica/Troll"),
|
||||
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||
("Asia/Aden", "Asia/Aden"),
|
||||
("Asia/Almaty", "Asia/Almaty"),
|
||||
("Asia/Amman", "Asia/Amman"),
|
||||
("Asia/Anadyr", "Asia/Anadyr"),
|
||||
("Asia/Aqtau", "Asia/Aqtau"),
|
||||
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||
("Asia/Ashkhabad", "Asia/Ashkhabad"),
|
||||
("Asia/Atyrau", "Asia/Atyrau"),
|
||||
("Asia/Baghdad", "Asia/Baghdad"),
|
||||
("Asia/Bahrain", "Asia/Bahrain"),
|
||||
("Asia/Baku", "Asia/Baku"),
|
||||
("Asia/Bangkok", "Asia/Bangkok"),
|
||||
("Asia/Barnaul", "Asia/Barnaul"),
|
||||
("Asia/Beirut", "Asia/Beirut"),
|
||||
("Asia/Bishkek", "Asia/Bishkek"),
|
||||
("Asia/Brunei", "Asia/Brunei"),
|
||||
("Asia/Calcutta", "Asia/Calcutta"),
|
||||
("Asia/Chita", "Asia/Chita"),
|
||||
("Asia/Choibalsan", "Asia/Choibalsan"),
|
||||
("Asia/Chongqing", "Asia/Chongqing"),
|
||||
("Asia/Chungking", "Asia/Chungking"),
|
||||
("Asia/Colombo", "Asia/Colombo"),
|
||||
("Asia/Dacca", "Asia/Dacca"),
|
||||
("Asia/Damascus", "Asia/Damascus"),
|
||||
("Asia/Dhaka", "Asia/Dhaka"),
|
||||
("Asia/Dili", "Asia/Dili"),
|
||||
("Asia/Dubai", "Asia/Dubai"),
|
||||
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||
("Asia/Famagusta", "Asia/Famagusta"),
|
||||
("Asia/Gaza", "Asia/Gaza"),
|
||||
("Asia/Harbin", "Asia/Harbin"),
|
||||
("Asia/Hebron", "Asia/Hebron"),
|
||||
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||
("Asia/Hovd", "Asia/Hovd"),
|
||||
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||
("Asia/Istanbul", "Asia/Istanbul"),
|
||||
("Asia/Jakarta", "Asia/Jakarta"),
|
||||
("Asia/Jayapura", "Asia/Jayapura"),
|
||||
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||
("Asia/Kabul", "Asia/Kabul"),
|
||||
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||
("Asia/Karachi", "Asia/Karachi"),
|
||||
("Asia/Kashgar", "Asia/Kashgar"),
|
||||
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||
("Asia/Katmandu", "Asia/Katmandu"),
|
||||
("Asia/Khandyga", "Asia/Khandyga"),
|
||||
("Asia/Kolkata", "Asia/Kolkata"),
|
||||
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||
("Asia/Kuching", "Asia/Kuching"),
|
||||
("Asia/Kuwait", "Asia/Kuwait"),
|
||||
("Asia/Macao", "Asia/Macao"),
|
||||
("Asia/Macau", "Asia/Macau"),
|
||||
("Asia/Magadan", "Asia/Magadan"),
|
||||
("Asia/Makassar", "Asia/Makassar"),
|
||||
("Asia/Manila", "Asia/Manila"),
|
||||
("Asia/Muscat", "Asia/Muscat"),
|
||||
("Asia/Nicosia", "Asia/Nicosia"),
|
||||
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||
("Asia/Omsk", "Asia/Omsk"),
|
||||
("Asia/Oral", "Asia/Oral"),
|
||||
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||
("Asia/Pontianak", "Asia/Pontianak"),
|
||||
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||
("Asia/Qatar", "Asia/Qatar"),
|
||||
("Asia/Qostanay", "Asia/Qostanay"),
|
||||
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||
("Asia/Rangoon", "Asia/Rangoon"),
|
||||
("Asia/Riyadh", "Asia/Riyadh"),
|
||||
("Asia/Saigon", "Asia/Saigon"),
|
||||
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||
("Asia/Samarkand", "Asia/Samarkand"),
|
||||
("Asia/Seoul", "Asia/Seoul"),
|
||||
("Asia/Shanghai", "Asia/Shanghai"),
|
||||
("Asia/Singapore", "Asia/Singapore"),
|
||||
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||
("Asia/Taipei", "Asia/Taipei"),
|
||||
("Asia/Tashkent", "Asia/Tashkent"),
|
||||
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||
("Asia/Tehran", "Asia/Tehran"),
|
||||
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
|
||||
("Asia/Thimbu", "Asia/Thimbu"),
|
||||
("Asia/Thimphu", "Asia/Thimphu"),
|
||||
("Asia/Tokyo", "Asia/Tokyo"),
|
||||
("Asia/Tomsk", "Asia/Tomsk"),
|
||||
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
|
||||
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
|
||||
("Asia/Urumqi", "Asia/Urumqi"),
|
||||
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||
("Asia/Vientiane", "Asia/Vientiane"),
|
||||
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||
("Asia/Yangon", "Asia/Yangon"),
|
||||
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||
("Asia/Yerevan", "Asia/Yerevan"),
|
||||
("Atlantic/Azores", "Atlantic/Azores"),
|
||||
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||
("Atlantic/Canary", "Atlantic/Canary"),
|
||||
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||
("Atlantic/Faeroe", "Atlantic/Faeroe"),
|
||||
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
|
||||
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||
("Australia/ACT", "Australia/ACT"),
|
||||
("Australia/Adelaide", "Australia/Adelaide"),
|
||||
("Australia/Brisbane", "Australia/Brisbane"),
|
||||
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||
("Australia/Canberra", "Australia/Canberra"),
|
||||
("Australia/Currie", "Australia/Currie"),
|
||||
("Australia/Darwin", "Australia/Darwin"),
|
||||
("Australia/Eucla", "Australia/Eucla"),
|
||||
("Australia/Hobart", "Australia/Hobart"),
|
||||
("Australia/LHI", "Australia/LHI"),
|
||||
("Australia/Lindeman", "Australia/Lindeman"),
|
||||
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||
("Australia/Melbourne", "Australia/Melbourne"),
|
||||
("Australia/NSW", "Australia/NSW"),
|
||||
("Australia/North", "Australia/North"),
|
||||
("Australia/Perth", "Australia/Perth"),
|
||||
("Australia/Queensland", "Australia/Queensland"),
|
||||
("Australia/South", "Australia/South"),
|
||||
("Australia/Sydney", "Australia/Sydney"),
|
||||
("Australia/Tasmania", "Australia/Tasmania"),
|
||||
("Australia/Victoria", "Australia/Victoria"),
|
||||
("Australia/West", "Australia/West"),
|
||||
("Australia/Yancowinna", "Australia/Yancowinna"),
|
||||
("Brazil/Acre", "Brazil/Acre"),
|
||||
("Brazil/DeNoronha", "Brazil/DeNoronha"),
|
||||
("Brazil/East", "Brazil/East"),
|
||||
("Brazil/West", "Brazil/West"),
|
||||
("CET", "CET"),
|
||||
("CST6CDT", "CST6CDT"),
|
||||
("Canada/Atlantic", "Canada/Atlantic"),
|
||||
("Canada/Central", "Canada/Central"),
|
||||
("Canada/Eastern", "Canada/Eastern"),
|
||||
("Canada/Mountain", "Canada/Mountain"),
|
||||
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||
("Canada/Pacific", "Canada/Pacific"),
|
||||
("Canada/Saskatchewan", "Canada/Saskatchewan"),
|
||||
("Canada/Yukon", "Canada/Yukon"),
|
||||
("Chile/Continental", "Chile/Continental"),
|
||||
("Chile/EasterIsland", "Chile/EasterIsland"),
|
||||
("Cuba", "Cuba"),
|
||||
("EET", "EET"),
|
||||
("EST", "EST"),
|
||||
("EST5EDT", "EST5EDT"),
|
||||
("Egypt", "Egypt"),
|
||||
("Eire", "Eire"),
|
||||
("Etc/GMT", "Etc/GMT"),
|
||||
("Etc/GMT+0", "Etc/GMT+0"),
|
||||
("Etc/GMT+1", "Etc/GMT+1"),
|
||||
("Etc/GMT+10", "Etc/GMT+10"),
|
||||
("Etc/GMT+11", "Etc/GMT+11"),
|
||||
("Etc/GMT+12", "Etc/GMT+12"),
|
||||
("Etc/GMT+2", "Etc/GMT+2"),
|
||||
("Etc/GMT+3", "Etc/GMT+3"),
|
||||
("Etc/GMT+4", "Etc/GMT+4"),
|
||||
("Etc/GMT+5", "Etc/GMT+5"),
|
||||
("Etc/GMT+6", "Etc/GMT+6"),
|
||||
("Etc/GMT+7", "Etc/GMT+7"),
|
||||
("Etc/GMT+8", "Etc/GMT+8"),
|
||||
("Etc/GMT+9", "Etc/GMT+9"),
|
||||
("Etc/GMT-0", "Etc/GMT-0"),
|
||||
("Etc/GMT-1", "Etc/GMT-1"),
|
||||
("Etc/GMT-10", "Etc/GMT-10"),
|
||||
("Etc/GMT-11", "Etc/GMT-11"),
|
||||
("Etc/GMT-12", "Etc/GMT-12"),
|
||||
("Etc/GMT-13", "Etc/GMT-13"),
|
||||
("Etc/GMT-14", "Etc/GMT-14"),
|
||||
("Etc/GMT-2", "Etc/GMT-2"),
|
||||
("Etc/GMT-3", "Etc/GMT-3"),
|
||||
("Etc/GMT-4", "Etc/GMT-4"),
|
||||
("Etc/GMT-5", "Etc/GMT-5"),
|
||||
("Etc/GMT-6", "Etc/GMT-6"),
|
||||
("Etc/GMT-7", "Etc/GMT-7"),
|
||||
("Etc/GMT-8", "Etc/GMT-8"),
|
||||
("Etc/GMT-9", "Etc/GMT-9"),
|
||||
("Etc/GMT0", "Etc/GMT0"),
|
||||
("Etc/Greenwich", "Etc/Greenwich"),
|
||||
("Etc/UCT", "Etc/UCT"),
|
||||
("Etc/UTC", "Etc/UTC"),
|
||||
("Etc/Universal", "Etc/Universal"),
|
||||
("Etc/Zulu", "Etc/Zulu"),
|
||||
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||
("Europe/Andorra", "Europe/Andorra"),
|
||||
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||
("Europe/Athens", "Europe/Athens"),
|
||||
("Europe/Belfast", "Europe/Belfast"),
|
||||
("Europe/Belgrade", "Europe/Belgrade"),
|
||||
("Europe/Berlin", "Europe/Berlin"),
|
||||
("Europe/Bratislava", "Europe/Bratislava"),
|
||||
("Europe/Brussels", "Europe/Brussels"),
|
||||
("Europe/Bucharest", "Europe/Bucharest"),
|
||||
("Europe/Budapest", "Europe/Budapest"),
|
||||
("Europe/Busingen", "Europe/Busingen"),
|
||||
("Europe/Chisinau", "Europe/Chisinau"),
|
||||
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||
("Europe/Dublin", "Europe/Dublin"),
|
||||
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||
("Europe/Guernsey", "Europe/Guernsey"),
|
||||
("Europe/Helsinki", "Europe/Helsinki"),
|
||||
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||
("Europe/Istanbul", "Europe/Istanbul"),
|
||||
("Europe/Jersey", "Europe/Jersey"),
|
||||
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||
("Europe/Kiev", "Europe/Kiev"),
|
||||
("Europe/Kirov", "Europe/Kirov"),
|
||||
("Europe/Kyiv", "Europe/Kyiv"),
|
||||
("Europe/Lisbon", "Europe/Lisbon"),
|
||||
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||
("Europe/London", "Europe/London"),
|
||||
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||
("Europe/Madrid", "Europe/Madrid"),
|
||||
("Europe/Malta", "Europe/Malta"),
|
||||
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||
("Europe/Minsk", "Europe/Minsk"),
|
||||
("Europe/Monaco", "Europe/Monaco"),
|
||||
("Europe/Moscow", "Europe/Moscow"),
|
||||
("Europe/Nicosia", "Europe/Nicosia"),
|
||||
("Europe/Oslo", "Europe/Oslo"),
|
||||
("Europe/Paris", "Europe/Paris"),
|
||||
("Europe/Podgorica", "Europe/Podgorica"),
|
||||
("Europe/Prague", "Europe/Prague"),
|
||||
("Europe/Riga", "Europe/Riga"),
|
||||
("Europe/Rome", "Europe/Rome"),
|
||||
("Europe/Samara", "Europe/Samara"),
|
||||
("Europe/San_Marino", "Europe/San_Marino"),
|
||||
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||
("Europe/Saratov", "Europe/Saratov"),
|
||||
("Europe/Simferopol", "Europe/Simferopol"),
|
||||
("Europe/Skopje", "Europe/Skopje"),
|
||||
("Europe/Sofia", "Europe/Sofia"),
|
||||
("Europe/Stockholm", "Europe/Stockholm"),
|
||||
("Europe/Tallinn", "Europe/Tallinn"),
|
||||
("Europe/Tirane", "Europe/Tirane"),
|
||||
("Europe/Tiraspol", "Europe/Tiraspol"),
|
||||
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||
("Europe/Uzhgorod", "Europe/Uzhgorod"),
|
||||
("Europe/Vaduz", "Europe/Vaduz"),
|
||||
("Europe/Vatican", "Europe/Vatican"),
|
||||
("Europe/Vienna", "Europe/Vienna"),
|
||||
("Europe/Vilnius", "Europe/Vilnius"),
|
||||
("Europe/Volgograd", "Europe/Volgograd"),
|
||||
("Europe/Warsaw", "Europe/Warsaw"),
|
||||
("Europe/Zagreb", "Europe/Zagreb"),
|
||||
("Europe/Zaporozhye", "Europe/Zaporozhye"),
|
||||
("Europe/Zurich", "Europe/Zurich"),
|
||||
("GB", "GB"),
|
||||
("GB-Eire", "GB-Eire"),
|
||||
("GMT", "GMT"),
|
||||
("GMT+0", "GMT+0"),
|
||||
("GMT-0", "GMT-0"),
|
||||
("GMT0", "GMT0"),
|
||||
("Greenwich", "Greenwich"),
|
||||
("HST", "HST"),
|
||||
("Hongkong", "Hongkong"),
|
||||
("Iceland", "Iceland"),
|
||||
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||
("Indian/Chagos", "Indian/Chagos"),
|
||||
("Indian/Christmas", "Indian/Christmas"),
|
||||
("Indian/Cocos", "Indian/Cocos"),
|
||||
("Indian/Comoro", "Indian/Comoro"),
|
||||
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||
("Indian/Mahe", "Indian/Mahe"),
|
||||
("Indian/Maldives", "Indian/Maldives"),
|
||||
("Indian/Mauritius", "Indian/Mauritius"),
|
||||
("Indian/Mayotte", "Indian/Mayotte"),
|
||||
("Indian/Reunion", "Indian/Reunion"),
|
||||
("Iran", "Iran"),
|
||||
("Israel", "Israel"),
|
||||
("Jamaica", "Jamaica"),
|
||||
("Japan", "Japan"),
|
||||
("Kwajalein", "Kwajalein"),
|
||||
("Libya", "Libya"),
|
||||
("MET", "MET"),
|
||||
("MST", "MST"),
|
||||
("MST7MDT", "MST7MDT"),
|
||||
("Mexico/BajaNorte", "Mexico/BajaNorte"),
|
||||
("Mexico/BajaSur", "Mexico/BajaSur"),
|
||||
("Mexico/General", "Mexico/General"),
|
||||
("NZ", "NZ"),
|
||||
("NZ-CHAT", "NZ-CHAT"),
|
||||
("Navajo", "Navajo"),
|
||||
("PRC", "PRC"),
|
||||
("PST8PDT", "PST8PDT"),
|
||||
("Pacific/Apia", "Pacific/Apia"),
|
||||
("Pacific/Auckland", "Pacific/Auckland"),
|
||||
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||
("Pacific/Chatham", "Pacific/Chatham"),
|
||||
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||
("Pacific/Easter", "Pacific/Easter"),
|
||||
("Pacific/Efate", "Pacific/Efate"),
|
||||
("Pacific/Enderbury", "Pacific/Enderbury"),
|
||||
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||
("Pacific/Fiji", "Pacific/Fiji"),
|
||||
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||
("Pacific/Gambier", "Pacific/Gambier"),
|
||||
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||
("Pacific/Guam", "Pacific/Guam"),
|
||||
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||
("Pacific/Johnston", "Pacific/Johnston"),
|
||||
("Pacific/Kanton", "Pacific/Kanton"),
|
||||
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||
("Pacific/Majuro", "Pacific/Majuro"),
|
||||
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||
("Pacific/Midway", "Pacific/Midway"),
|
||||
("Pacific/Nauru", "Pacific/Nauru"),
|
||||
("Pacific/Niue", "Pacific/Niue"),
|
||||
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||
("Pacific/Noumea", "Pacific/Noumea"),
|
||||
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||
("Pacific/Palau", "Pacific/Palau"),
|
||||
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||
("Pacific/Ponape", "Pacific/Ponape"),
|
||||
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||
("Pacific/Saipan", "Pacific/Saipan"),
|
||||
("Pacific/Samoa", "Pacific/Samoa"),
|
||||
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||
("Pacific/Truk", "Pacific/Truk"),
|
||||
("Pacific/Wake", "Pacific/Wake"),
|
||||
("Pacific/Wallis", "Pacific/Wallis"),
|
||||
("Pacific/Yap", "Pacific/Yap"),
|
||||
("Poland", "Poland"),
|
||||
("Portugal", "Portugal"),
|
||||
("ROC", "ROC"),
|
||||
("ROK", "ROK"),
|
||||
("Singapore", "Singapore"),
|
||||
("Turkey", "Turkey"),
|
||||
("UCT", "UCT"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("US/Arizona", "US/Arizona"),
|
||||
("US/Central", "US/Central"),
|
||||
("US/East-Indiana", "US/East-Indiana"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("US/Indiana-Starke", "US/Indiana-Starke"),
|
||||
("US/Michigan", "US/Michigan"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
("UTC", "UTC"),
|
||||
("Universal", "Universal"),
|
||||
("W-SU", "W-SU"),
|
||||
("WET", "WET"),
|
||||
("Zulu", "Zulu"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.3 on 2023-07-18 01:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0037_coresettings_open_ai_model_and_more"),
|
||||
("agents", "0056_alter_agent_time_zone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="agentcustomfield",
|
||||
unique_together={("agent", "field")},
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import re
|
||||
from collections import Counter
|
||||
from distutils.version import LooseVersion
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
|
||||
|
||||
import msgpack
|
||||
@@ -15,8 +15,10 @@ from django.db import models
|
||||
from django.utils import timezone as djangotime
|
||||
from nats.errors import TimeoutError
|
||||
from packaging import version as pyver
|
||||
from packaging.version import Version as LooseVersion
|
||||
|
||||
from agents.utils import get_agent_url
|
||||
from checks.models import CheckResult
|
||||
from core.models import TZ_CHOICES
|
||||
from core.utils import get_core_settings, send_command_with_mesh
|
||||
from logs.models import BaseAuditModel, DebugLog, PendingAction
|
||||
@@ -24,6 +26,7 @@ from tacticalrmm.constants import (
|
||||
AGENT_STATUS_OFFLINE,
|
||||
AGENT_STATUS_ONLINE,
|
||||
AGENT_STATUS_OVERDUE,
|
||||
AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX,
|
||||
ONLINE_AGENTS,
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
@@ -37,7 +40,7 @@ from tacticalrmm.constants import (
|
||||
PAAction,
|
||||
PAStatus,
|
||||
)
|
||||
from tacticalrmm.helpers import get_nats_ports
|
||||
from tacticalrmm.helpers import setup_nats_options
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -130,8 +133,8 @@ class Agent(BaseAuditModel):
|
||||
# return the default timezone unless the timezone is explicity set per agent
|
||||
if self.time_zone:
|
||||
return self.time_zone
|
||||
else:
|
||||
return get_core_settings().default_time_zone
|
||||
|
||||
return get_core_settings().default_time_zone
|
||||
|
||||
@property
|
||||
def is_posix(self) -> bool:
|
||||
@@ -198,8 +201,9 @@ class Agent(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
|
||||
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
|
||||
now = djangotime.now()
|
||||
offline = now - djangotime.timedelta(minutes=self.offline_time)
|
||||
overdue = now - djangotime.timedelta(minutes=self.overdue_time)
|
||||
|
||||
if self.last_seen is not None:
|
||||
if (self.last_seen < offline) and (self.last_seen > overdue):
|
||||
@@ -213,8 +217,6 @@ class Agent(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def checks(self) -> Dict[str, Any]:
|
||||
from checks.models import CheckResult
|
||||
|
||||
total, passing, failing, warning, info = 0, 0, 0, 0, 0
|
||||
|
||||
for check in self.get_checks_with_policies(exclude_overridden=True):
|
||||
@@ -232,12 +234,12 @@ class Agent(BaseAuditModel):
|
||||
alert_severity = (
|
||||
check.check_result.alert_severity
|
||||
if check.check_type
|
||||
in [
|
||||
in (
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
)
|
||||
else check.alert_severity
|
||||
)
|
||||
if alert_severity == AlertSeverity.ERROR:
|
||||
@@ -257,6 +259,15 @@ class Agent(BaseAuditModel):
|
||||
}
|
||||
return ret
|
||||
|
||||
@property
|
||||
def pending_actions_count(self) -> int:
|
||||
ret = cache.get(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}")
|
||||
if ret is None:
|
||||
ret = self.pendingactions.filter(status=PAStatus.PENDING).count()
|
||||
cache.set(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}", ret, 600)
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def cpu_model(self) -> List[str]:
|
||||
if self.is_posix:
|
||||
@@ -333,8 +344,8 @@ class Agent(BaseAuditModel):
|
||||
|
||||
if len(ret) == 1:
|
||||
return cast(str, ret[0])
|
||||
else:
|
||||
return ", ".join(ret) if ret else "error getting local ips"
|
||||
|
||||
return ", ".join(ret) if ret else "error getting local ips"
|
||||
|
||||
@property
|
||||
def make_model(self) -> str:
|
||||
@@ -344,7 +355,7 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
return "error getting make/model"
|
||||
|
||||
try:
|
||||
with suppress(Exception):
|
||||
comp_sys = self.wmi_detail["comp_sys"][0]
|
||||
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
|
||||
make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
|
||||
@@ -361,14 +372,10 @@ class Agent(BaseAuditModel):
|
||||
model = sysfam
|
||||
|
||||
return f"{make} {model}"
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
with suppress(Exception):
|
||||
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
|
||||
return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0])
|
||||
except:
|
||||
pass
|
||||
|
||||
return "unknown make/model"
|
||||
|
||||
@@ -401,6 +408,16 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
return ["unknown disk"]
|
||||
|
||||
@property
|
||||
def serial_number(self) -> str:
|
||||
if self.is_posix:
|
||||
return ""
|
||||
|
||||
try:
|
||||
return self.wmi_detail["bios"][0][0]["SerialNumber"]
|
||||
except:
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def online_agents(cls, min_version: str = "") -> "List[Agent]":
|
||||
if min_version:
|
||||
@@ -423,7 +440,6 @@ class Agent(BaseAuditModel):
|
||||
def get_checks_with_policies(
|
||||
self, exclude_overridden: bool = False
|
||||
) -> "List[Check]":
|
||||
|
||||
if exclude_overridden:
|
||||
checks = (
|
||||
list(
|
||||
@@ -438,12 +454,10 @@ class Agent(BaseAuditModel):
|
||||
return self.add_check_results(checks)
|
||||
|
||||
def get_tasks_with_policies(self) -> "List[AutomatedTask]":
|
||||
|
||||
tasks = list(self.autotasks.all()) + self.get_tasks_from_policies()
|
||||
return self.add_task_results(tasks)
|
||||
|
||||
def add_task_results(self, tasks: "List[AutomatedTask]") -> "List[AutomatedTask]":
|
||||
|
||||
results = self.taskresults.all() # type: ignore
|
||||
|
||||
for task in tasks:
|
||||
@@ -455,7 +469,6 @@ class Agent(BaseAuditModel):
|
||||
return tasks
|
||||
|
||||
def add_check_results(self, checks: "List[Check]") -> "List[Check]":
|
||||
|
||||
results = self.checkresults.all() # type: ignore
|
||||
|
||||
for check in checks:
|
||||
@@ -479,7 +492,7 @@ class Agent(BaseAuditModel):
|
||||
models.prefetch_related_objects(
|
||||
[
|
||||
policy
|
||||
for policy in [self.policy, site_policy, client_policy, default_policy]
|
||||
for policy in (self.policy, site_policy, client_policy, default_policy)
|
||||
if policy
|
||||
],
|
||||
"excluded_agents",
|
||||
@@ -517,7 +530,6 @@ class Agent(BaseAuditModel):
|
||||
# determine if any agent checks have a custom interval and set the lowest interval
|
||||
for check in self.get_checks_with_policies():
|
||||
if check.run_interval and check.run_interval < interval:
|
||||
|
||||
# don't allow check runs less than 15s
|
||||
interval = 15 if check.run_interval < 15 else check.run_interval
|
||||
|
||||
@@ -532,13 +544,19 @@ class Agent(BaseAuditModel):
|
||||
wait: bool = False,
|
||||
run_on_any: bool = False,
|
||||
history_pk: int = 0,
|
||||
run_as_user: bool = False,
|
||||
env_vars: list[str] = [],
|
||||
) -> Any:
|
||||
|
||||
from scripts.models import Script
|
||||
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
|
||||
# always override if set on script model
|
||||
if script.run_as_user:
|
||||
run_as_user = True
|
||||
|
||||
parsed_args = script.parse_script_args(self, script.shell, args)
|
||||
parsed_env_vars = script.parse_script_env_vars(self, script.shell, env_vars)
|
||||
|
||||
data = {
|
||||
"func": "runscriptfull" if full else "runscript",
|
||||
@@ -548,6 +566,8 @@ class Agent(BaseAuditModel):
|
||||
"code": script.code,
|
||||
"shell": script.shell,
|
||||
},
|
||||
"run_as_user": run_as_user,
|
||||
"env_vars": parsed_env_vars,
|
||||
}
|
||||
|
||||
if history_pk != 0:
|
||||
@@ -583,7 +603,7 @@ class Agent(BaseAuditModel):
|
||||
def approve_updates(self) -> None:
|
||||
patch_policy = self.get_patch_policy()
|
||||
|
||||
severity_list = list()
|
||||
severity_list = []
|
||||
if patch_policy.critical == "approve":
|
||||
severity_list.append("Critical")
|
||||
|
||||
@@ -615,17 +635,14 @@ class Agent(BaseAuditModel):
|
||||
if not agent_policy:
|
||||
agent_policy = WinUpdatePolicy.objects.create(agent=self)
|
||||
|
||||
# Get the list of policies applied to the agent and select the
|
||||
# highest priority one.
|
||||
policies = self.get_agent_policies()
|
||||
|
||||
processed_policies: List[int] = list()
|
||||
for _, policy in policies.items():
|
||||
if (
|
||||
policy
|
||||
and policy.active
|
||||
and policy.pk not in processed_policies
|
||||
and policy.winupdatepolicy.exists()
|
||||
):
|
||||
if policy and policy.active and policy.winupdatepolicy.exists():
|
||||
patch_policy = policy.winupdatepolicy.first()
|
||||
break
|
||||
|
||||
# if policy still doesn't exist return the agent patch policy
|
||||
if not patch_policy:
|
||||
@@ -677,7 +694,7 @@ class Agent(BaseAuditModel):
|
||||
policies = self.get_agent_policies()
|
||||
|
||||
# loop through all policies applied to agent and return an alert_template if found
|
||||
processed_policies: List[int] = list()
|
||||
processed_policies: List[int] = []
|
||||
for key, policy in policies.items():
|
||||
# default alert_template will override a default policy with alert template applied
|
||||
if (
|
||||
@@ -742,10 +759,10 @@ class Agent(BaseAuditModel):
|
||||
cache_key = f"agent_{self.agent_id}_checks"
|
||||
|
||||
elif self.policy:
|
||||
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_checks"
|
||||
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks"
|
||||
|
||||
else:
|
||||
cache_key = f"site_{self.monitoring_type}_{self.site_id}_checks"
|
||||
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks"
|
||||
|
||||
cached_checks = cache.get(cache_key)
|
||||
if isinstance(cached_checks, list):
|
||||
@@ -767,10 +784,10 @@ class Agent(BaseAuditModel):
|
||||
cache_key = f"agent_{self.agent_id}_tasks"
|
||||
|
||||
elif self.policy:
|
||||
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_tasks"
|
||||
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks"
|
||||
|
||||
else:
|
||||
cache_key = f"site_{self.monitoring_type}_{self.site_id}_tasks"
|
||||
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks"
|
||||
|
||||
cached_tasks = cache.get(cache_key)
|
||||
if isinstance(cached_tasks, list):
|
||||
@@ -778,7 +795,7 @@ class Agent(BaseAuditModel):
|
||||
else:
|
||||
# get agent tasks based on policies
|
||||
tasks = Policy.get_policy_tasks(self)
|
||||
cache.set(f"site_{self.site_id}_tasks", tasks, 600)
|
||||
cache.set(cache_key, tasks, 600)
|
||||
return tasks
|
||||
|
||||
def _do_nats_debug(self, agent: "Agent", message: str) -> None:
|
||||
@@ -787,17 +804,9 @@ class Agent(BaseAuditModel):
|
||||
async def nats_cmd(
|
||||
self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
|
||||
) -> Any:
|
||||
nats_std_port, _ = get_nats_ports()
|
||||
options = {
|
||||
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
|
||||
"user": "tacticalrmm",
|
||||
"password": settings.SECRET_KEY,
|
||||
"connect_timeout": 3,
|
||||
"max_reconnect_attempts": 2,
|
||||
}
|
||||
|
||||
opts = setup_nats_options()
|
||||
try:
|
||||
nc = await nats.connect(**options)
|
||||
nc = await nats.connect(**opts)
|
||||
except:
|
||||
return "natsdown"
|
||||
|
||||
@@ -829,9 +838,12 @@ class Agent(BaseAuditModel):
|
||||
Return type: tuple(message: str, error: bool)
|
||||
"""
|
||||
if mode == "tacagent":
|
||||
if self.is_posix:
|
||||
if self.plat == AgentPlat.LINUX:
|
||||
cmd = "systemctl restart tacticalagent.service"
|
||||
shell = 3
|
||||
elif self.plat == AgentPlat.DARWIN:
|
||||
cmd = "launchctl kickstart -k system/tacticalagent"
|
||||
shell = 3
|
||||
else:
|
||||
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
|
||||
shell = 1
|
||||
@@ -839,22 +851,22 @@ class Agent(BaseAuditModel):
|
||||
asyncio.run(
|
||||
send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
|
||||
)
|
||||
return ("ok", False)
|
||||
return "ok", False
|
||||
|
||||
elif mode == "mesh":
|
||||
data = {"func": "recover", "payload": {"mode": mode}}
|
||||
if wait:
|
||||
r = asyncio.run(self.nats_cmd(data, timeout=20))
|
||||
if r == "ok":
|
||||
return ("ok", False)
|
||||
return "ok", False
|
||||
else:
|
||||
return (str(r), True)
|
||||
return str(r), True
|
||||
else:
|
||||
asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
|
||||
|
||||
return ("ok", False)
|
||||
return "ok", False
|
||||
|
||||
return ("invalid", True)
|
||||
return "invalid", True
|
||||
|
||||
@staticmethod
|
||||
def serialize(agent: "Agent") -> Dict[str, Any]:
|
||||
@@ -864,7 +876,7 @@ class Agent(BaseAuditModel):
|
||||
return AgentAuditSerializer(agent).data
|
||||
|
||||
def delete_superseded_updates(self) -> None:
|
||||
try:
|
||||
with suppress(Exception):
|
||||
pks = [] # list of pks to delete
|
||||
kbs = list(self.winupdates.values_list("kb", flat=True))
|
||||
d = Counter(kbs)
|
||||
@@ -875,8 +887,10 @@ class Agent(BaseAuditModel):
|
||||
# extract the version from the title and sort from oldest to newest
|
||||
# skip if no version info is available therefore nothing to parse
|
||||
try:
|
||||
matches = r"(Version|Versão)"
|
||||
pattern = r"\(" + matches + r"(.*?)\)"
|
||||
vers = [
|
||||
re.search(r"\(Version(.*?)\)", i).group(1).strip()
|
||||
re.search(pattern, i, flags=re.IGNORECASE).group(2).strip()
|
||||
for i in titles
|
||||
]
|
||||
sorted_vers = sorted(vers, key=LooseVersion)
|
||||
@@ -889,8 +903,6 @@ class Agent(BaseAuditModel):
|
||||
|
||||
pks = list(set(pks))
|
||||
self.winupdates.filter(pk__in=pks).delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
def should_create_alert(
|
||||
self, alert_template: "Optional[AlertTemplate]" = None
|
||||
@@ -1000,6 +1012,9 @@ class AgentCustomField(models.Model):
|
||||
default=list,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("agent", "field"),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.field.name
|
||||
|
||||
@@ -1009,16 +1024,16 @@ class AgentCustomField(models.Model):
|
||||
return cast(List[str], self.multiple_value)
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return cast(str, self.string_value)
|
||||
|
||||
return cast(str, self.string_value)
|
||||
|
||||
def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
|
||||
if self.field.type in [
|
||||
if self.field.type in (
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
):
|
||||
self.string_value = cast(str, value)
|
||||
self.save()
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
|
||||
@@ -96,10 +96,8 @@ class RunScriptPerms(permissions.BasePermission):
|
||||
|
||||
class AgentNotesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
|
||||
# permissions for GET /agents/notes/ endpoint
|
||||
if r.method == "GET":
|
||||
|
||||
# permissions for /agents/<agent_id>/notes endpoint
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
|
||||
@@ -122,5 +120,15 @@ class AgentHistoryPerms(permissions.BasePermission):
|
||||
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_agent_history")
|
||||
|
||||
return _has_perm(r, "can_list_agent_history")
|
||||
|
||||
|
||||
class AgentWOLPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_send_wol") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
return _has_perm(r, "can_send_wol")
|
||||
|
||||
@@ -90,26 +90,32 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
last_seen = serializers.ReadOnlyField()
|
||||
pending_actions_count = serializers.ReadOnlyField()
|
||||
has_patches_pending = serializers.ReadOnlyField()
|
||||
cpu_model = serializers.ReadOnlyField()
|
||||
graphics = serializers.ReadOnlyField()
|
||||
local_ips = serializers.ReadOnlyField()
|
||||
make_model = serializers.ReadOnlyField()
|
||||
physical_disks = serializers.ReadOnlyField()
|
||||
serial_number = serializers.ReadOnlyField()
|
||||
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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_logged_username(self, obj) -> str:
|
||||
if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE:
|
||||
return obj.last_logged_in_user
|
||||
elif obj.logged_in_username != "None":
|
||||
return obj.logged_in_username
|
||||
else:
|
||||
return "-"
|
||||
|
||||
return "-"
|
||||
|
||||
def get_italic(self, obj) -> bool:
|
||||
return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE
|
||||
@@ -141,16 +147,20 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"plat",
|
||||
"goarch",
|
||||
"has_patches_pending",
|
||||
"version",
|
||||
"operating_system",
|
||||
"public_ip",
|
||||
"cpu_model",
|
||||
"graphics",
|
||||
"local_ips",
|
||||
"make_model",
|
||||
"physical_disks",
|
||||
"custom_fields",
|
||||
"serial_number",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
||||
class WinAgentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AgentHostnameSerializer(serializers.ModelSerializer):
|
||||
client = serializers.ReadOnlyField(source="client.name")
|
||||
site = serializers.ReadOnlyField(source="site.name")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import datetime as dt
|
||||
import random
|
||||
from time import sleep
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
@@ -13,10 +12,13 @@ from scripts.models import Script
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_DEFER,
|
||||
AGENT_OUTAGES_LOCK,
|
||||
AGENT_STATUS_OVERDUE,
|
||||
CheckStatus,
|
||||
DebugLogType,
|
||||
)
|
||||
from tacticalrmm.helpers import rand_range
|
||||
from tacticalrmm.utils import redis_lock
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models.query import QuerySet
|
||||
@@ -46,7 +48,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
|
||||
return "alert not found"
|
||||
|
||||
if not alert.email_sent:
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
alert.agent.send_outage_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -55,7 +57,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
|
||||
# send an email only if the last email sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.email_sent < delta:
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
alert.agent.send_outage_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -67,7 +69,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
|
||||
def agent_recovery_email_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
@@ -91,7 +93,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
|
||||
return "alert not found"
|
||||
|
||||
if not alert.sms_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
alert.agent.send_outage_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -100,7 +102,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
|
||||
# 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, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
alert.agent.send_outage_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -112,7 +114,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
|
||||
def agent_recovery_sms_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -125,24 +127,20 @@ def agent_recovery_sms_task(pk: int) -> str:
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_outages_task() -> None:
|
||||
from alerts.models import Alert
|
||||
@app.task(bind=True)
|
||||
def agent_outages_task(self) -> str:
|
||||
with redis_lock(AGENT_OUTAGES_LOCK, self.app.oid) as acquired:
|
||||
if not acquired:
|
||||
return f"{self.app.oid} still running"
|
||||
|
||||
agents = Agent.objects.only(
|
||||
"pk",
|
||||
"agent_id",
|
||||
"last_seen",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
)
|
||||
from alerts.models import Alert
|
||||
from core.tasks import _get_agent_qs
|
||||
|
||||
for agent in agents:
|
||||
if agent.status == AGENT_STATUS_OVERDUE:
|
||||
Alert.handle_alert_failure(agent)
|
||||
for agent in _get_agent_qs():
|
||||
if agent.status == AGENT_STATUS_OVERDUE:
|
||||
Alert.handle_alert_failure(agent)
|
||||
|
||||
return "completed"
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -153,6 +151,8 @@ def run_script_email_results_task(
|
||||
emails: list[str],
|
||||
args: list[str] = [],
|
||||
history_pk: int = 0,
|
||||
run_as_user: bool = False,
|
||||
env_vars: list[str] = [],
|
||||
):
|
||||
agent = Agent.objects.get(pk=agentpk)
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
@@ -163,6 +163,8 @@ def run_script_email_results_task(
|
||||
timeout=nats_timeout,
|
||||
wait=True,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
if r == "timeout":
|
||||
DebugLog.error(
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
site=self.site1,
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
plat=AgentPlat.WINDOWS,
|
||||
version="2.3.0",
|
||||
version="2.1.1",
|
||||
)
|
||||
r = agent_noarch.do_update(token="", force=True)
|
||||
self.assertEqual(r, "noarch")
|
||||
@@ -106,7 +106,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
site=self.site1,
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
plat=AgentPlat.WINDOWS,
|
||||
version="2.3.0",
|
||||
version="2.1.1",
|
||||
goarch=GoArch.AMD64,
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
site=self.site3,
|
||||
monitoring_type=AgentMonType.WORKSTATION,
|
||||
plat=AgentPlat.LINUX,
|
||||
version="2.3.0",
|
||||
version="2.1.1",
|
||||
goarch=GoArch.ARM32,
|
||||
)
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
site=self.site2,
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
plat=AgentPlat.WINDOWS,
|
||||
version="2.3.0",
|
||||
version="2.1.1",
|
||||
goarch=GoArch.AMD64,
|
||||
_quantity=6,
|
||||
)
|
||||
@@ -215,7 +215,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
site=self.site2,
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
plat=AgentPlat.WINDOWS,
|
||||
version="2.3.0",
|
||||
version="2.1.1",
|
||||
goarch=GoArch.AMD64,
|
||||
_quantity=7,
|
||||
)
|
||||
@@ -264,7 +264,7 @@ class TestAgentUpdate(TacticalTestCase):
|
||||
agents = baker.make_recipe("agents.agent", _quantity=5)
|
||||
other_agents = baker.make_recipe("agents.agent", _quantity=7)
|
||||
|
||||
url = f"/agents/update/"
|
||||
url = "/agents/update/"
|
||||
|
||||
data = {
|
||||
"agent_ids": [agent.agent_id for agent in agents]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from unittest.mock import patch, AsyncMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.utils import generate_linux_install, get_agent_url
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
@@ -50,7 +49,7 @@ class TestAgentUtils(TacticalTestCase):
|
||||
|
||||
self.assertIn(r"agentDL='asdasd3423'", ret)
|
||||
self.assertIn(
|
||||
r"meshDL='meshsite/meshagents?id=meshid&installflags=0&meshinstall=6'", ret
|
||||
r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret
|
||||
)
|
||||
self.assertIn(r"apiURL='api.example.com'", ret)
|
||||
self.assertIn(r"agentDL='asdasd3423'", ret)
|
||||
|
||||
@@ -3,8 +3,8 @@ import os
|
||||
from itertools import cycle
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
@@ -403,6 +403,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
"cmd": "ipconfig",
|
||||
"shell": "cmd",
|
||||
"timeout": 30,
|
||||
"run_as_user": False,
|
||||
}
|
||||
mock_ret.return_value = "nt authority\\system"
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -417,16 +418,20 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_reboot_later(self, nats_cmd):
|
||||
nats_cmd.return_value = "ok"
|
||||
url = f"{base_url}/{self.agent.agent_id}/reboot/"
|
||||
|
||||
data = {
|
||||
"datetime": "2025-08-29T18:41:02",
|
||||
}
|
||||
# ensure we don't allow dates in past
|
||||
data = {"datetime": "2022-07-11T01:51"}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "Date cannot be set in the past")
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
# test with date in future
|
||||
data["datetime"] = "2027-08-29T18:41"
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
|
||||
self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
|
||||
self.assertEqual(r.data["agent"], self.agent.hostname)
|
||||
|
||||
nats_data = {
|
||||
@@ -439,12 +444,12 @@ class TestAgentViews(TacticalTestCase):
|
||||
"multiple_instances": 2,
|
||||
"trigger": "runonce",
|
||||
"name": r.data["task_name"],
|
||||
"start_year": 2025,
|
||||
"start_year": 2027,
|
||||
"start_month": 8,
|
||||
"start_day": 29,
|
||||
"start_hour": 18,
|
||||
"start_min": 41,
|
||||
"expire_year": 2025,
|
||||
"expire_year": 2027,
|
||||
"expire_month": 8,
|
||||
"expire_day": 29,
|
||||
"expire_hour": 18,
|
||||
@@ -534,6 +539,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"output": "wait",
|
||||
"args": [],
|
||||
"timeout": 15,
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -543,7 +550,13 @@ class TestAgentViews(TacticalTestCase):
|
||||
raise AgentHistory.DoesNotExist
|
||||
|
||||
run_script.assert_called_with(
|
||||
scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=hist.pk
|
||||
scriptpk=script.pk,
|
||||
args=[],
|
||||
timeout=18,
|
||||
wait=True,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -555,15 +568,21 @@ class TestAgentViews(TacticalTestCase):
|
||||
"timeout": 15,
|
||||
"emailMode": "default",
|
||||
"emails": ["admin@example.com", "bob@example.com"],
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
|
||||
email_task.assert_called_with(
|
||||
agentpk=self.agent.pk,
|
||||
scriptpk=script.pk,
|
||||
nats_timeout=18,
|
||||
emails=[],
|
||||
args=["abc", "123"],
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
email_task.reset_mock()
|
||||
|
||||
@@ -571,12 +590,16 @@ class TestAgentViews(TacticalTestCase):
|
||||
data["emailMode"] = "custom"
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
|
||||
email_task.assert_called_with(
|
||||
agentpk=self.agent.pk,
|
||||
scriptpk=script.pk,
|
||||
nats_timeout=18,
|
||||
emails=["admin@example.com", "bob@example.com"],
|
||||
args=["abc", "123"],
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
|
||||
# test fire and forget
|
||||
@@ -585,6 +608,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"output": "forget",
|
||||
"args": ["hello", "world"],
|
||||
"timeout": 22,
|
||||
"run_as_user": True,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -594,7 +619,12 @@ class TestAgentViews(TacticalTestCase):
|
||||
raise AgentHistory.DoesNotExist
|
||||
|
||||
run_script.assert_called_with(
|
||||
scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=hist.pk
|
||||
scriptpk=script.pk,
|
||||
args=["hello", "world"],
|
||||
timeout=25,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=True,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -609,6 +639,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"timeout": 22,
|
||||
"custom_field": custom_field.pk,
|
||||
"save_all_output": True,
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -623,6 +655,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
timeout=25,
|
||||
wait=True,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -640,6 +674,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"timeout": 22,
|
||||
"custom_field": custom_field.pk,
|
||||
"save_all_output": False,
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -654,6 +690,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
timeout=25,
|
||||
wait=True,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -673,6 +711,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"timeout": 22,
|
||||
"custom_field": custom_field.pk,
|
||||
"save_all_output": False,
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -687,6 +727,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
timeout=25,
|
||||
wait=True,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -703,6 +745,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
"output": "note",
|
||||
"args": ["hello", "world"],
|
||||
"timeout": 22,
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -717,6 +761,8 @@ class TestAgentViews(TacticalTestCase):
|
||||
timeout=25,
|
||||
wait=True,
|
||||
history_pk=hist.pk,
|
||||
run_as_user=False,
|
||||
env_vars=["hello=world", "foo=bar"],
|
||||
)
|
||||
run_script.reset_mock()
|
||||
|
||||
@@ -809,7 +855,6 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_agent_history(self):
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
history = baker.make("agents.AgentHistory", agent=agent, _quantity=30)
|
||||
@@ -821,7 +866,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
# test pulling data
|
||||
r = self.client.get(url, format="json")
|
||||
ctx = {"default_tz": pytz.timezone("America/Los_Angeles")}
|
||||
ctx = {"default_tz": ZoneInfo("America/Los_Angeles")}
|
||||
data = AgentHistorySerializer(history, many=True, context=ctx).data
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, data) # type:ignore
|
||||
@@ -965,7 +1010,6 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
@patch("time.sleep")
|
||||
@patch("agents.models.Agent.nats_cmd", return_value="ok")
|
||||
def test_agent_actions_permissions(self, nats_cmd, sleep):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
|
||||
@@ -1093,7 +1137,6 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
self.assertEqual(len(response.data["agents"]), 7)
|
||||
|
||||
def test_generating_agent_installer_permissions(self):
|
||||
|
||||
client = baker.make("clients.Client")
|
||||
client_site = baker.make("clients.Site", client=client)
|
||||
site = baker.make("clients.Site")
|
||||
@@ -1156,7 +1199,6 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
def test_agent_notes_permissions(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
notes = baker.make("agents.Note", agent=agent, _quantity=5)
|
||||
|
||||
@@ -1245,9 +1287,9 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
|
||||
sites = baker.make("clients.Site", _quantity=2)
|
||||
agent = baker.make_recipe("agents.agent", site=sites[0])
|
||||
history = baker.make("agents.AgentHistory", agent=agent, _quantity=5)
|
||||
history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) # noqa
|
||||
unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1])
|
||||
unauthorized_history = baker.make(
|
||||
unauthorized_history = baker.make( # noqa
|
||||
"agents.AgentHistory", agent=unauthorized_agent, _quantity=6
|
||||
)
|
||||
|
||||
|
||||
@@ -42,4 +42,6 @@ urlpatterns = [
|
||||
path("update/", views.update_agents),
|
||||
path("installer/", views.install_agent),
|
||||
path("bulkrecovery/", views.bulk_agent_recovery),
|
||||
path("scripthistory/", views.ScriptRunHistory.as_view()),
|
||||
path("<agent:agent_id>/wol/", views.wol),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse
|
||||
@@ -33,7 +34,6 @@ def generate_linux_install(
|
||||
api: str,
|
||||
download_url: str,
|
||||
) -> FileResponse:
|
||||
|
||||
match arch:
|
||||
case "amd64":
|
||||
arch_id = MeshAgentIdent.LINUX64
|
||||
@@ -51,12 +51,10 @@ def generate_linux_install(
|
||||
uri = get_mesh_ws_url()
|
||||
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
|
||||
mesh_dl = (
|
||||
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"
|
||||
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}"
|
||||
)
|
||||
|
||||
sh = settings.LINUX_AGENT_SCRIPT
|
||||
with open(sh, "r") as f:
|
||||
text = f.read()
|
||||
text = Path(settings.LINUX_AGENT_SCRIPT).read_text()
|
||||
|
||||
replace = {
|
||||
"agentDLChange": download_url,
|
||||
@@ -71,11 +69,8 @@ def generate_linux_install(
|
||||
for i, j in replace.items():
|
||||
text = text.replace(i, j)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with open(fp.name, "w") as f:
|
||||
f.write(text)
|
||||
f.write("\n")
|
||||
|
||||
text += "\n"
|
||||
with StringIO(text) as fp:
|
||||
return FileResponse(
|
||||
open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh"
|
||||
fp.read(), as_attachment=True, filename="linux_agent_install.sh"
|
||||
)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from meshctrl.utils import get_login_token
|
||||
from packaging import version as pyver
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
@@ -23,30 +26,32 @@ from core.utils import (
|
||||
get_mesh_ws_url,
|
||||
remove_mesh_agent,
|
||||
token_is_valid,
|
||||
wake_on_lan,
|
||||
)
|
||||
from logs.models import AuditLog, DebugLog, PendingAction
|
||||
from scripts.models import Script
|
||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
|
||||
from scripts.tasks import bulk_command_task, bulk_script_task
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_DEFER,
|
||||
AGENT_STATUS_OFFLINE,
|
||||
AGENT_STATUS_ONLINE,
|
||||
AGENT_TABLE_DEFER,
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
AgentPlat,
|
||||
CustomFieldModel,
|
||||
DebugLogType,
|
||||
EvtLogNames,
|
||||
PAAction,
|
||||
PAStatus,
|
||||
)
|
||||
from tacticalrmm.helpers import notify_error
|
||||
from tacticalrmm.helpers import date_is_in_past, notify_error
|
||||
from tacticalrmm.permissions import (
|
||||
_has_perm_on_agent,
|
||||
_has_perm_on_client,
|
||||
_has_perm_on_site,
|
||||
)
|
||||
from tacticalrmm.utils import get_default_timezone, reload_nats
|
||||
from winupdate.models import WinUpdate
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||
|
||||
@@ -55,6 +60,7 @@ from .permissions import (
|
||||
AgentHistoryPerms,
|
||||
AgentNotesPerms,
|
||||
AgentPerms,
|
||||
AgentWOLPerms,
|
||||
EvtLogPerms,
|
||||
InstallAgentPerms,
|
||||
ManageProcPerms,
|
||||
@@ -113,7 +119,7 @@ class GetAgents(APIView):
|
||||
Agent.objects.filter_by_role(request.user) # type: ignore
|
||||
.filter(monitoring_type_filter)
|
||||
.filter(client_site_filter)
|
||||
.defer(*AGENT_DEFER)
|
||||
.defer(*AGENT_TABLE_DEFER)
|
||||
.select_related(
|
||||
"site__server_policy",
|
||||
"site__workstation_policy",
|
||||
@@ -131,19 +137,17 @@ class GetAgents(APIView):
|
||||
"checkresults",
|
||||
queryset=CheckResult.objects.select_related("assigned_check"),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
pending_actions_count=Count(
|
||||
"pendingactions",
|
||||
filter=Q(pendingactions__status=PAStatus.PENDING),
|
||||
)
|
||||
Prefetch(
|
||||
"custom_fields",
|
||||
queryset=AgentCustomField.objects.select_related("field"),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
has_patches_pending=Exists(
|
||||
WinUpdate.objects.filter(
|
||||
agent_id=OuterRef("pk"), action="approve", installed=False
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
serializer = AgentTableSerializer(agents, many=True)
|
||||
@@ -165,18 +169,66 @@ class GetAgents(APIView):
|
||||
class GetUpdateDeleteAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
|
||||
class InputSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"maintenance_mode", # TODO separate this
|
||||
"policy", # TODO separate this
|
||||
"block_policy_inheritance", # TODO separate this
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
"check_interval",
|
||||
"time_zone",
|
||||
"site",
|
||||
]
|
||||
|
||||
# get agent details
|
||||
def get(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
from checks.models import Check, CheckResult
|
||||
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.select_related(
|
||||
"site__server_policy",
|
||||
"site__workstation_policy",
|
||||
"site__client__server_policy",
|
||||
"site__client__workstation_policy",
|
||||
"policy",
|
||||
"alert_template",
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"agentchecks",
|
||||
queryset=Check.objects.select_related("script"),
|
||||
),
|
||||
Prefetch(
|
||||
"checkresults",
|
||||
queryset=CheckResult.objects.select_related("assigned_check"),
|
||||
),
|
||||
Prefetch(
|
||||
"custom_fields",
|
||||
queryset=AgentCustomField.objects.select_related("field"),
|
||||
),
|
||||
Prefetch(
|
||||
"winupdatepolicy",
|
||||
queryset=WinUpdatePolicy.objects.select_related("agent", "policy"),
|
||||
),
|
||||
),
|
||||
agent_id=agent_id,
|
||||
)
|
||||
return Response(AgentSerializer(agent).data)
|
||||
|
||||
# edit agent
|
||||
def put(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
a_serializer.is_valid(raise_exception=True)
|
||||
a_serializer.save()
|
||||
s = self.InputSerializer(instance=agent, data=request.data, partial=True)
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
|
||||
if "winupdatepolicy" in request.data.keys():
|
||||
policy = agent.winupdatepolicy.get() # type: ignore
|
||||
@@ -187,9 +239,7 @@ class GetUpdateDeleteAgent(APIView):
|
||||
p_serializer.save()
|
||||
|
||||
if "custom_fields" in request.data.keys():
|
||||
|
||||
for field in request.data["custom_fields"]:
|
||||
|
||||
custom_field = field
|
||||
custom_field["agent"] = agent.pk
|
||||
|
||||
@@ -215,18 +265,25 @@ class GetUpdateDeleteAgent(APIView):
|
||||
def delete(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
code = "foo"
|
||||
code = "foo" # stub for windows
|
||||
if agent.plat == AgentPlat.LINUX:
|
||||
with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
|
||||
code = f.read()
|
||||
code = Path(settings.LINUX_AGENT_SCRIPT).read_text()
|
||||
elif agent.plat == AgentPlat.DARWIN:
|
||||
code = Path(settings.MAC_UNINSTALL).read_text()
|
||||
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
|
||||
name = agent.hostname
|
||||
mesh_id = agent.mesh_node_id
|
||||
agent.delete()
|
||||
reload_nats()
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(remove_mesh_agent(uri, mesh_id))
|
||||
try:
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(remove_mesh_agent(uri, mesh_id))
|
||||
except Exception as e:
|
||||
DebugLog.error(
|
||||
message=f"Unable to remove agent {name} from meshcentral database: {e}",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
)
|
||||
return Response(f"{name} will now be uninstalled.")
|
||||
|
||||
|
||||
@@ -242,7 +299,7 @@ class AgentProcesses(APIView):
|
||||
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
if r in ("timeout", "natsdown"):
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response(r)
|
||||
|
||||
@@ -253,7 +310,7 @@ class AgentProcesses(APIView):
|
||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||
)
|
||||
|
||||
if r == "timeout" or r == "natsdown":
|
||||
if r in ("timeout", "natsdown"):
|
||||
return notify_error("Unable to contact the agent")
|
||||
elif r != "ok":
|
||||
return notify_error(r)
|
||||
@@ -385,7 +442,7 @@ def get_event_log(request, agent_id, logtype, days):
|
||||
},
|
||||
}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
if r in ("timeout", "natsdown"):
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
return Response(r)
|
||||
@@ -408,6 +465,7 @@ def send_raw_cmd(request, agent_id):
|
||||
"command": request.data["cmd"],
|
||||
"shell": shell,
|
||||
},
|
||||
"run_as_user": request.data["run_as_user"],
|
||||
}
|
||||
|
||||
hist = AgentHistory.objects.create(
|
||||
@@ -436,6 +494,7 @@ def send_raw_cmd(request, agent_id):
|
||||
|
||||
class Reboot(APIView):
|
||||
permission_classes = [IsAuthenticated, RebootAgentPerms]
|
||||
|
||||
# reboot now
|
||||
def post(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
@@ -452,10 +511,13 @@ class Reboot(APIView):
|
||||
return notify_error(f"Not currently implemented for {agent.plat}")
|
||||
|
||||
try:
|
||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S")
|
||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M")
|
||||
except Exception:
|
||||
return notify_error("Invalid date")
|
||||
|
||||
if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone):
|
||||
return notify_error("Date cannot be set in the past")
|
||||
|
||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
||||
random.choice(string.ascii_letters) for _ in range(10)
|
||||
)
|
||||
@@ -508,6 +570,20 @@ def install_agent(request):
|
||||
from agents.utils import get_agent_url
|
||||
from core.utils import token_is_valid
|
||||
|
||||
insecure = getattr(settings, "TRMM_INSECURE", False)
|
||||
|
||||
if insecure and request.data["installMethod"] in {"exe", "powershell"}:
|
||||
return notify_error(
|
||||
"Not available in insecure mode. Please use the 'Manual' method."
|
||||
)
|
||||
|
||||
# TODO rework this ghetto validation hack
|
||||
# https://github.com/amidaware/tacticalrmm/issues/1461
|
||||
try:
|
||||
int(request.data["expires"])
|
||||
except ValueError:
|
||||
return notify_error("Please enter a valid number of hours")
|
||||
|
||||
client_id = request.data["client"]
|
||||
site_id = request.data["site"]
|
||||
version = settings.LATEST_AGENT_VER
|
||||
@@ -519,15 +595,38 @@ def install_agent(request):
|
||||
|
||||
codesign_token, is_valid = token_is_valid()
|
||||
|
||||
inno = f"tacticalagent-v{version}-{plat}-{goarch}.exe"
|
||||
if request.data["installMethod"] in {"bash", "mac"} and not is_valid:
|
||||
return notify_error(
|
||||
"Linux/Mac agents require code signing. Please see https://docs.tacticalrmm.com/code_signing/ for more info."
|
||||
)
|
||||
|
||||
inno = f"tacticalagent-v{version}-{plat}-{goarch}"
|
||||
if plat == AgentPlat.WINDOWS:
|
||||
inno += ".exe"
|
||||
|
||||
download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token)
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
_, token = AuthToken.objects.create(
|
||||
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
|
||||
user=installer_user, expiry=dt.timedelta(hours=int(request.data["expires"]))
|
||||
)
|
||||
|
||||
install_flags = [
|
||||
"-m",
|
||||
"install",
|
||||
"--api",
|
||||
request.data["api"],
|
||||
"--client-id",
|
||||
client_id,
|
||||
"--site-id",
|
||||
site_id,
|
||||
"--agent-type",
|
||||
request.data["agenttype"],
|
||||
"--auth",
|
||||
token,
|
||||
]
|
||||
|
||||
if request.data["installMethod"] == "exe":
|
||||
from tacticalrmm.utils import generate_winagent_exe
|
||||
|
||||
@@ -545,15 +644,6 @@ def install_agent(request):
|
||||
)
|
||||
|
||||
elif request.data["installMethod"] == "bash":
|
||||
# TODO
|
||||
# linux agents are in beta for now, only available for sponsors for testing
|
||||
# remove this after it's out of beta
|
||||
|
||||
if not is_valid:
|
||||
return notify_error(
|
||||
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
|
||||
)
|
||||
|
||||
from agents.utils import generate_linux_install
|
||||
|
||||
return generate_linux_install(
|
||||
@@ -566,52 +656,49 @@ def install_agent(request):
|
||||
download_url=download_url,
|
||||
)
|
||||
|
||||
elif request.data["installMethod"] == "manual":
|
||||
cmd = [
|
||||
inno,
|
||||
"/VERYSILENT",
|
||||
"/SUPPRESSMSGBOXES",
|
||||
"&&",
|
||||
"ping",
|
||||
"127.0.0.1",
|
||||
"-n",
|
||||
"5",
|
||||
"&&",
|
||||
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
|
||||
"-m",
|
||||
"install",
|
||||
"--api",
|
||||
request.data["api"],
|
||||
"--client-id",
|
||||
client_id,
|
||||
"--site-id",
|
||||
site_id,
|
||||
"--agent-type",
|
||||
request.data["agenttype"],
|
||||
"--auth",
|
||||
token,
|
||||
]
|
||||
elif request.data["installMethod"] in {"manual", "mac"}:
|
||||
resp = {}
|
||||
if request.data["installMethod"] == "manual":
|
||||
cmd = [
|
||||
inno,
|
||||
"/VERYSILENT",
|
||||
"/SUPPRESSMSGBOXES",
|
||||
"&&",
|
||||
"ping",
|
||||
"127.0.0.1",
|
||||
"-n",
|
||||
"5",
|
||||
"&&",
|
||||
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
|
||||
] + install_flags
|
||||
|
||||
if int(request.data["rdp"]):
|
||||
cmd.append("--rdp")
|
||||
if int(request.data["ping"]):
|
||||
cmd.append("--ping")
|
||||
if int(request.data["power"]):
|
||||
cmd.append("--power")
|
||||
if int(request.data["rdp"]):
|
||||
cmd.append("--rdp")
|
||||
if int(request.data["ping"]):
|
||||
cmd.append("--ping")
|
||||
if int(request.data["power"]):
|
||||
cmd.append("--power")
|
||||
|
||||
resp = {
|
||||
"cmd": " ".join(str(i) for i in cmd),
|
||||
"url": download_url,
|
||||
}
|
||||
if insecure:
|
||||
cmd.append("--insecure")
|
||||
|
||||
resp["cmd"] = " ".join(str(i) for i in cmd)
|
||||
else:
|
||||
install_flags.insert(0, f"sudo ./{inno}")
|
||||
cmd = install_flags.copy()
|
||||
dl = f"curl -L -o {inno} '{download_url}'"
|
||||
resp["cmd"] = (
|
||||
dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd)
|
||||
)
|
||||
if insecure:
|
||||
resp["cmd"] += " --insecure"
|
||||
|
||||
resp["url"] = download_url
|
||||
|
||||
return Response(resp)
|
||||
|
||||
elif request.data["installMethod"] == "powershell":
|
||||
|
||||
ps = os.path.join(settings.BASE_DIR, "core/installer.ps1")
|
||||
|
||||
with open(ps, "r") as f:
|
||||
text = f.read()
|
||||
text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text()
|
||||
|
||||
replace_dict = {
|
||||
"innosetupchange": inno,
|
||||
@@ -629,27 +716,9 @@ def install_agent(request):
|
||||
for i, j in replace_dict.items():
|
||||
text = text.replace(i, j)
|
||||
|
||||
file_name = "rmm-installer.ps1"
|
||||
ps1 = os.path.join(settings.EXE_DIR, file_name)
|
||||
|
||||
if os.path.exists(ps1):
|
||||
try:
|
||||
os.remove(ps1)
|
||||
except Exception as e:
|
||||
DebugLog.error(message=str(e))
|
||||
|
||||
with open(ps1, "w") as f:
|
||||
f.write(text)
|
||||
|
||||
if settings.DEBUG:
|
||||
with open(ps1, "r") as f:
|
||||
response = HttpResponse(f.read(), content_type="text/plain")
|
||||
response["Content-Disposition"] = f"inline; filename={file_name}"
|
||||
return response
|
||||
else:
|
||||
response = HttpResponse()
|
||||
response["Content-Disposition"] = f"attachment; filename={file_name}"
|
||||
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
|
||||
with StringIO(text) as fp:
|
||||
response = HttpResponse(fp.read(), content_type="text/plain")
|
||||
response["Content-Disposition"] = "attachment; filename=rmm-installer.ps1"
|
||||
return response
|
||||
|
||||
|
||||
@@ -681,6 +750,8 @@ def run_script(request, agent_id):
|
||||
script = get_object_or_404(Script, pk=request.data["script"])
|
||||
output = request.data["output"]
|
||||
args = request.data["args"]
|
||||
run_as_user: bool = request.data["run_as_user"]
|
||||
env_vars: list[str] = request.data["env_vars"]
|
||||
req_timeout = int(request.data["timeout"]) + 3
|
||||
|
||||
AuditLog.audit_script_run(
|
||||
@@ -705,6 +776,8 @@ def run_script(request, agent_id):
|
||||
timeout=req_timeout,
|
||||
wait=True,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
return Response(r)
|
||||
|
||||
@@ -718,6 +791,9 @@ def run_script(request, agent_id):
|
||||
nats_timeout=req_timeout,
|
||||
emails=emails,
|
||||
args=args,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
elif output == "collector":
|
||||
from core.models import CustomField
|
||||
@@ -728,6 +804,8 @@ def run_script(request, agent_id):
|
||||
timeout=req_timeout,
|
||||
wait=True,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
|
||||
custom_field = CustomField.objects.get(pk=request.data["custom_field"])
|
||||
@@ -756,13 +834,20 @@ def run_script(request, agent_id):
|
||||
timeout=req_timeout,
|
||||
wait=True,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
|
||||
Note.objects.create(agent=agent, user=request.user, note=r)
|
||||
return Response(r)
|
||||
else:
|
||||
agent.run_script(
|
||||
scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
|
||||
scriptpk=script.pk,
|
||||
args=args,
|
||||
timeout=req_timeout,
|
||||
history_pk=history_pk,
|
||||
run_as_user=run_as_user,
|
||||
env_vars=env_vars,
|
||||
)
|
||||
|
||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||
@@ -872,11 +957,13 @@ def bulk(request):
|
||||
q = q.filter(plat=AgentPlat.WINDOWS)
|
||||
elif request.data["osType"] == AgentPlat.LINUX:
|
||||
q = q.filter(plat=AgentPlat.LINUX)
|
||||
elif request.data["osType"] == AgentPlat.DARWIN:
|
||||
q = q.filter(plat=AgentPlat.DARWIN)
|
||||
|
||||
agents: list[int] = [agent.pk for agent in q]
|
||||
|
||||
if not agents:
|
||||
return notify_error("No agents where found meeting the selected criteria")
|
||||
return notify_error("No agents were found meeting the selected criteria")
|
||||
|
||||
AuditLog.audit_bulk_action(
|
||||
request.user,
|
||||
@@ -891,29 +978,32 @@ def bulk(request):
|
||||
else:
|
||||
shell = request.data["shell"]
|
||||
|
||||
handle_bulk_command_task.delay(
|
||||
agents,
|
||||
request.data["cmd"],
|
||||
shell,
|
||||
request.data["timeout"],
|
||||
request.user.username[:50],
|
||||
run_on_offline=request.data["offlineAgents"],
|
||||
bulk_command_task.delay(
|
||||
agent_pks=agents,
|
||||
cmd=request.data["cmd"],
|
||||
shell=shell,
|
||||
timeout=request.data["timeout"],
|
||||
username=request.user.username[:50],
|
||||
run_as_user=request.data["run_as_user"],
|
||||
)
|
||||
return Response(f"Command will now be run on {len(agents)} agents")
|
||||
|
||||
elif request.data["mode"] == "script":
|
||||
script = get_object_or_404(Script, pk=request.data["script"])
|
||||
handle_bulk_script_task.delay(
|
||||
script.pk,
|
||||
agents,
|
||||
request.data["args"],
|
||||
request.data["timeout"],
|
||||
request.user.username[:50],
|
||||
|
||||
bulk_script_task.delay(
|
||||
script_pk=script.pk,
|
||||
agent_pks=agents,
|
||||
args=request.data["args"],
|
||||
timeout=request.data["timeout"],
|
||||
username=request.user.username[:50],
|
||||
run_as_user=request.data["run_as_user"],
|
||||
env_vars=request.data["env_vars"],
|
||||
)
|
||||
|
||||
return Response(f"{script.name} will now be run on {len(agents)} agents")
|
||||
|
||||
elif request.data["mode"] == "patch":
|
||||
|
||||
if request.data["patchMode"] == "install":
|
||||
bulk_install_updates_task.delay(agents)
|
||||
return Response(
|
||||
@@ -929,7 +1019,6 @@ def bulk(request):
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, AgentPerms])
|
||||
def agent_maintenance(request):
|
||||
|
||||
if request.data["type"] == "Client":
|
||||
if not _has_perm_on_client(request.user, request.data["id"]):
|
||||
raise PermissionDenied()
|
||||
@@ -956,10 +1045,10 @@ def agent_maintenance(request):
|
||||
if count:
|
||||
action = "disabled" if not request.data["action"] else "enabled"
|
||||
return Response(f"Maintenance mode has been {action} on {count} agents")
|
||||
else:
|
||||
return Response(
|
||||
f"No agents have been put in maintenance mode. You might not have permissions to the resources."
|
||||
)
|
||||
|
||||
return Response(
|
||||
"No agents have been put in maintenance mode. You might not have permissions to the resources."
|
||||
)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@@ -991,3 +1080,113 @@ class AgentHistoryView(APIView):
|
||||
history = AgentHistory.objects.filter_by_role(request.user) # type: ignore
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
return Response(AgentHistorySerializer(history, many=True, context=ctx).data)
|
||||
|
||||
|
||||
class ScriptRunHistory(APIView):
|
||||
permission_classes = [IsAuthenticated, AgentHistoryPerms]
|
||||
|
||||
class OutputSerializer(serializers.ModelSerializer):
|
||||
script_name = serializers.ReadOnlyField(source="script.name")
|
||||
agent_id = serializers.ReadOnlyField(source="agent.agent_id")
|
||||
|
||||
class Meta:
|
||||
model = AgentHistory
|
||||
fields = (
|
||||
"id",
|
||||
"time",
|
||||
"username",
|
||||
"script",
|
||||
"script_results",
|
||||
"agent",
|
||||
"script_name",
|
||||
"agent_id",
|
||||
)
|
||||
read_only_fields = fields
|
||||
|
||||
def get(self, request):
|
||||
date_range_filter = Q()
|
||||
script_name_filter = Q()
|
||||
|
||||
start = request.query_params.get("start", None)
|
||||
end = request.query_params.get("end", None)
|
||||
limit = request.query_params.get("limit", None)
|
||||
script_name = request.query_params.get("scriptname", None)
|
||||
if start and end:
|
||||
start_dt = parse_datetime(start)
|
||||
end_dt = parse_datetime(end) + djangotime.timedelta(days=1)
|
||||
date_range_filter = Q(time__range=[start_dt, end_dt])
|
||||
|
||||
if script_name:
|
||||
script_name_filter = Q(script__name=script_name)
|
||||
|
||||
AGENT_R_DEFER = (
|
||||
"agent__wmi_detail",
|
||||
"agent__services",
|
||||
"agent__created_by",
|
||||
"agent__created_time",
|
||||
"agent__modified_by",
|
||||
"agent__modified_time",
|
||||
"agent__disks",
|
||||
"agent__operating_system",
|
||||
"agent__mesh_node_id",
|
||||
"agent__description",
|
||||
"agent__patches_last_installed",
|
||||
"agent__time_zone",
|
||||
"agent__alert_template_id",
|
||||
"agent__policy_id",
|
||||
"agent__site_id",
|
||||
"agent__version",
|
||||
"agent__plat",
|
||||
"agent__goarch",
|
||||
"agent__hostname",
|
||||
"agent__last_seen",
|
||||
"agent__public_ip",
|
||||
"agent__total_ram",
|
||||
"agent__boot_time",
|
||||
"agent__logged_in_username",
|
||||
"agent__last_logged_in_user",
|
||||
"agent__monitoring_type",
|
||||
"agent__overdue_email_alert",
|
||||
"agent__overdue_text_alert",
|
||||
"agent__overdue_dashboard_alert",
|
||||
"agent__offline_time",
|
||||
"agent__overdue_time",
|
||||
"agent__check_interval",
|
||||
"agent__needs_reboot",
|
||||
"agent__choco_installed",
|
||||
"agent__maintenance_mode",
|
||||
"agent__block_policy_inheritance",
|
||||
)
|
||||
hists = (
|
||||
AgentHistory.objects.filter(type=AgentHistoryType.SCRIPT_RUN)
|
||||
.select_related("agent")
|
||||
.select_related("script")
|
||||
.defer(*AGENT_R_DEFER)
|
||||
.filter(date_range_filter)
|
||||
.filter(script_name_filter)
|
||||
.order_by("-time")
|
||||
)
|
||||
if limit:
|
||||
try:
|
||||
lim = int(limit)
|
||||
except KeyError:
|
||||
return notify_error("Invalid limit")
|
||||
hists = hists[:lim]
|
||||
|
||||
ret = self.OutputSerializer(hists, many=True).data
|
||||
return Response(ret)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, AgentWOLPerms])
|
||||
def wol(request, agent_id):
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER),
|
||||
agent_id=agent_id,
|
||||
)
|
||||
try:
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(wake_on_lan(uri=uri, mesh_node_id=agent.mesh_node_id))
|
||||
except Exception as e:
|
||||
return notify_error(str(e))
|
||||
return Response(f"Wake-on-LAN sent to {agent.hostname}")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-26 20:22
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("alerts", "0012_alter_alert_action_retcode_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="alerttemplate",
|
||||
name="action_env_vars",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(blank=True, null=True),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="alerttemplate",
|
||||
name="resolved_action_env_vars",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(blank=True, null=True),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -10,6 +10,7 @@ from django.utils import timezone as djangotime
|
||||
|
||||
from logs.models import BaseAuditModel, DebugLog
|
||||
from tacticalrmm.constants import (
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
AlertSeverity,
|
||||
AlertType,
|
||||
@@ -153,7 +154,6 @@ class Alert(models.Model):
|
||||
alert_severity: Optional[str] = None,
|
||||
skip_create: bool = False,
|
||||
) -> "Optional[Alert]":
|
||||
|
||||
# need to pass agent if the check is a policy
|
||||
if not cls.objects.filter(
|
||||
assigned_check=check,
|
||||
@@ -171,12 +171,12 @@ class Alert(models.Model):
|
||||
alert_type=AlertType.CHECK,
|
||||
severity=check.alert_severity
|
||||
if check.check_type
|
||||
not in [
|
||||
not in {
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
}
|
||||
else alert_severity,
|
||||
message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
|
||||
hidden=True,
|
||||
@@ -216,7 +216,6 @@ class Alert(models.Model):
|
||||
agent: "Agent",
|
||||
skip_create: bool = False,
|
||||
) -> "Optional[Alert]":
|
||||
|
||||
if not cls.objects.filter(
|
||||
assigned_task=task,
|
||||
agent=agent,
|
||||
@@ -268,7 +267,7 @@ class Alert(models.Model):
|
||||
def handle_alert_failure(
|
||||
cls, instance: Union[Agent, TaskResult, CheckResult]
|
||||
) -> None:
|
||||
from agents.models import Agent
|
||||
from agents.models import Agent, AgentHistory
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
|
||||
@@ -327,12 +326,12 @@ class Alert(models.Model):
|
||||
alert_severity = (
|
||||
instance.assigned_check.alert_severity
|
||||
if instance.assigned_check.check_type
|
||||
not in [
|
||||
not in {
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
}
|
||||
else instance.alert_severity
|
||||
)
|
||||
agent = instance.agent
|
||||
@@ -341,23 +340,20 @@ class Alert(models.Model):
|
||||
if alert_template:
|
||||
dashboard_severities = (
|
||||
alert_template.check_dashboard_alert_severity
|
||||
if alert_template.check_dashboard_alert_severity
|
||||
else [
|
||||
or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
AlertSeverity.INFO,
|
||||
]
|
||||
)
|
||||
email_severities = (
|
||||
alert_template.check_email_alert_severity
|
||||
if alert_template.check_email_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
text_severities = (
|
||||
alert_template.check_text_alert_severity
|
||||
if alert_template.check_text_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
email_severities = alert_template.check_email_alert_severity or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
]
|
||||
text_severities = alert_template.check_text_alert_severity or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
]
|
||||
always_dashboard = alert_template.check_always_alert
|
||||
always_email = alert_template.check_always_email
|
||||
always_text = alert_template.check_always_text
|
||||
@@ -380,21 +376,18 @@ class Alert(models.Model):
|
||||
|
||||
# set alert_template settings
|
||||
if alert_template:
|
||||
dashboard_severities = (
|
||||
alert_template.task_dashboard_alert_severity
|
||||
if alert_template.task_dashboard_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
email_severities = (
|
||||
alert_template.task_email_alert_severity
|
||||
if alert_template.task_email_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
text_severities = (
|
||||
alert_template.task_text_alert_severity
|
||||
if alert_template.task_text_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
dashboard_severities = alert_template.task_dashboard_alert_severity or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
]
|
||||
email_severities = alert_template.task_email_alert_severity or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
]
|
||||
text_severities = alert_template.task_text_alert_severity or [
|
||||
AlertSeverity.ERROR,
|
||||
AlertSeverity.WARNING,
|
||||
]
|
||||
always_dashboard = alert_template.task_always_alert
|
||||
always_email = alert_template.task_always_email
|
||||
always_text = alert_template.task_always_text
|
||||
@@ -417,7 +410,6 @@ class Alert(models.Model):
|
||||
|
||||
# create alert in dashboard if enabled
|
||||
if dashboard_alert or always_dashboard:
|
||||
|
||||
# check if alert template is set and specific severities are configured
|
||||
if (
|
||||
not alert_template
|
||||
@@ -430,7 +422,6 @@ class Alert(models.Model):
|
||||
|
||||
# send email if enabled
|
||||
if email_alert or always_email:
|
||||
|
||||
# check if alert template is set and specific severities are configured
|
||||
if (
|
||||
not alert_template
|
||||
@@ -445,7 +436,6 @@ class Alert(models.Model):
|
||||
|
||||
# send text if enabled
|
||||
if text_alert or always_text:
|
||||
|
||||
# check if alert template is set and specific severities are configured
|
||||
if (
|
||||
not alert_template
|
||||
@@ -462,13 +452,22 @@ class Alert(models.Model):
|
||||
and run_script_action
|
||||
and not alert.action_run
|
||||
):
|
||||
hist = AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type=AgentHistoryType.SCRIPT_RUN,
|
||||
script=alert_template.action,
|
||||
username="alert-action-failure",
|
||||
)
|
||||
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,
|
||||
history_pk=hist.pk,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
run_as_user=False,
|
||||
env_vars=alert_template.action_env_vars,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
@@ -490,7 +489,7 @@ class Alert(models.Model):
|
||||
def handle_alert_resolve(
|
||||
cls, instance: Union[Agent, TaskResult, CheckResult]
|
||||
) -> None:
|
||||
from agents.models import Agent
|
||||
from agents.models import Agent, AgentHistory
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
|
||||
@@ -584,13 +583,22 @@ class Alert(models.Model):
|
||||
and run_script_action
|
||||
and not alert.resolved_action_run
|
||||
):
|
||||
hist = AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type=AgentHistoryType.SCRIPT_RUN,
|
||||
script=alert_template.action,
|
||||
username="alert-action-resolved",
|
||||
)
|
||||
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,
|
||||
history_pk=hist.pk,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
run_as_user=False,
|
||||
env_vars=alert_template.resolved_action_env_vars,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
@@ -611,17 +619,15 @@ class Alert(models.Model):
|
||||
)
|
||||
|
||||
def parse_script_args(self, args: List[str]) -> List[str]:
|
||||
|
||||
if not args:
|
||||
return []
|
||||
|
||||
temp_args = list()
|
||||
temp_args = []
|
||||
# pattern to match for injection
|
||||
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
|
||||
|
||||
for arg in args:
|
||||
match = pattern.match(arg)
|
||||
if match:
|
||||
if match := pattern.match(arg):
|
||||
name = match.group(1)
|
||||
|
||||
# check if attr exists and isn't a function
|
||||
@@ -632,6 +638,8 @@ class Alert(models.Model):
|
||||
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
|
||||
except re.error:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", re.escape(value), arg))
|
||||
except Exception as e:
|
||||
DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e))
|
||||
continue
|
||||
@@ -659,6 +667,12 @@ class AlertTemplate(BaseAuditModel):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
action_env_vars = ArrayField(
|
||||
models.TextField(null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
action_timeout = models.PositiveIntegerField(default=15)
|
||||
resolved_action = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
@@ -673,6 +687,12 @@ class AlertTemplate(BaseAuditModel):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
resolved_action_env_vars = ArrayField(
|
||||
models.TextField(null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
resolved_action_timeout = models.PositiveIntegerField(default=15)
|
||||
|
||||
# overrides the global recipients
|
||||
|
||||
@@ -32,7 +32,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
|
||||
|
||||
class AlertPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
if r.method in ("GET", "PATCH"):
|
||||
if "pk" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
|
||||
r.user, view.kwargs["pk"]
|
||||
@@ -52,5 +52,5 @@ class AlertTemplatePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_alerttemplates")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_alerttemplates")
|
||||
|
||||
return _has_perm(r, "can_manage_alerttemplates")
|
||||
|
||||
@@ -8,7 +8,6 @@ from .models import Alert, AlertTemplate
|
||||
|
||||
|
||||
class AlertSerializer(ModelSerializer):
|
||||
|
||||
hostname = ReadOnlyField(source="assigned_agent.hostname")
|
||||
agent_id = ReadOnlyField(source="assigned_agent.agent_id")
|
||||
client = ReadOnlyField(source="client.name")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -8,7 +8,7 @@ from model_bakery import baker, seq
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from autotasks.models import TaskResult
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
from core.tasks import cache_db_fields_task, resolve_alerts_task
|
||||
from core.utils import get_core_settings
|
||||
from tacticalrmm.constants import AgentMonType, AlertSeverity, AlertType, CheckStatus
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
@@ -28,6 +28,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
"""
|
||||
def test_get_alerts(self):
|
||||
url = "/alerts/"
|
||||
|
||||
@@ -39,14 +40,14 @@ class TestAlertsViews(TacticalTestCase):
|
||||
alerts = baker.make(
|
||||
"alerts.Alert",
|
||||
agent=agent,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
alert_time=seq(djangotime.now(), timedelta(days=15)),
|
||||
severity=AlertSeverity.WARNING,
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
assigned_check=check,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
alert_time=seq(djangotime.now(), timedelta(days=15)),
|
||||
severity=AlertSeverity.ERROR,
|
||||
_quantity=7,
|
||||
)
|
||||
@@ -55,7 +56,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
assigned_task=task,
|
||||
snoozed=True,
|
||||
snooze_until=djangotime.now(),
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
alert_time=seq(djangotime.now(), timedelta(days=15)),
|
||||
_quantity=2,
|
||||
)
|
||||
baker.make(
|
||||
@@ -63,7 +64,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
agent=agent,
|
||||
resolved=True,
|
||||
resolved_on=djangotime.now(),
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
alert_time=seq(djangotime.now(), timedelta(days=15)),
|
||||
_quantity=9,
|
||||
)
|
||||
|
||||
@@ -120,13 +121,14 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.assertEqual(len(resp.data), req["count"])
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
"""
|
||||
|
||||
def test_add_alert(self):
|
||||
url = "/alerts/"
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
data = {
|
||||
"alert_time": datetime.now(),
|
||||
"alert_time": djangotime.now(),
|
||||
"agent": agent.id,
|
||||
"severity": "warning",
|
||||
"alert_type": "availability",
|
||||
@@ -363,7 +365,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
not_snoozed = baker.make(
|
||||
"alerts.Alert",
|
||||
snoozed=True,
|
||||
snooze_until=seq(datetime.now(), timedelta(days=15)),
|
||||
snooze_until=seq(djangotime.now(), timedelta(days=15)),
|
||||
_quantity=5,
|
||||
)
|
||||
|
||||
@@ -371,7 +373,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
snoozed = baker.make(
|
||||
"alerts.Alert",
|
||||
snoozed=True,
|
||||
snooze_until=seq(datetime.now(), timedelta(days=-15)),
|
||||
snooze_until=seq(djangotime.now(), timedelta(days=-15)),
|
||||
_quantity=5,
|
||||
)
|
||||
|
||||
@@ -389,7 +391,6 @@ class TestAlertTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
def test_agent_gets_correct_alert_template(self):
|
||||
|
||||
core = get_core_settings()
|
||||
# setup data
|
||||
workstation = baker.make_recipe(
|
||||
@@ -677,8 +678,6 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent_template_email = Agent.objects.get(pk=agent_template_email.pk)
|
||||
|
||||
# have the two agents checkin
|
||||
url = "/api/v3/checkin/"
|
||||
|
||||
agent_template_text.version = settings.LATEST_AGENT_VER
|
||||
agent_template_text.last_seen = djangotime.now()
|
||||
agent_template_text.save()
|
||||
@@ -688,7 +687,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent_template_email.save()
|
||||
|
||||
cache_db_fields_task()
|
||||
handle_resolved_stuff()
|
||||
resolve_alerts_task()
|
||||
|
||||
recovery_sms.assert_called_with(
|
||||
pk=Alert.objects.get(agent=agent_template_text).pk
|
||||
@@ -1373,7 +1372,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
def test_alert_actions(
|
||||
self, recovery_sms, recovery_email, outage_email, outage_sms, nats_cmd
|
||||
):
|
||||
|
||||
from agents.models import AgentHistory
|
||||
from agents.tasks import agent_outages_task
|
||||
|
||||
# Setup cmd mock
|
||||
@@ -1399,9 +1398,12 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent_script_actions=False,
|
||||
action=failure_action,
|
||||
action_timeout=30,
|
||||
action_args=["hello", "world"],
|
||||
action_env_vars=["hello=world", "foo=bar"],
|
||||
resolved_action=resolved_action,
|
||||
resolved_action_timeout=35,
|
||||
resolved_action_args=["nice_arg"],
|
||||
resolved_action_env_vars=["resolved=action", "env=vars"],
|
||||
)
|
||||
agent.client.alert_template = alert_template
|
||||
agent.client.save()
|
||||
@@ -1422,8 +1424,11 @@ class TestAlertTasks(TacticalTestCase):
|
||||
data = {
|
||||
"func": "runscriptfull",
|
||||
"timeout": 30,
|
||||
"script_args": [],
|
||||
"script_args": ["hello", "world"],
|
||||
"payload": {"code": failure_action.code, "shell": failure_action.shell},
|
||||
"run_as_user": False,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
"id": AgentHistory.objects.last().pk, # type: ignore
|
||||
}
|
||||
|
||||
nats_cmd.assert_called_with(data, timeout=30, wait=True)
|
||||
@@ -1444,7 +1449,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent.save()
|
||||
|
||||
cache_db_fields_task()
|
||||
handle_resolved_stuff()
|
||||
resolve_alerts_task()
|
||||
|
||||
# this is what data should be
|
||||
data = {
|
||||
@@ -1452,6 +1457,9 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"timeout": 35,
|
||||
"script_args": ["nice_arg"],
|
||||
"payload": {"code": resolved_action.code, "shell": resolved_action.shell},
|
||||
"run_as_user": False,
|
||||
"env_vars": ["resolved=action", "env=vars"],
|
||||
"id": AgentHistory.objects.last().pk, # type: ignore
|
||||
}
|
||||
|
||||
nats_cmd.assert_called_with(data, timeout=35, wait=True)
|
||||
@@ -1625,8 +1633,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
unauthorized_task_url,
|
||||
]
|
||||
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
for method in ("get", "put", "delete"):
|
||||
# test superuser access
|
||||
for url in authorized_urls:
|
||||
self.check_authorized_superuser(method, url)
|
||||
|
||||
@@ -23,15 +23,18 @@ class GetAddAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, AlertPerms]
|
||||
|
||||
def patch(self, request):
|
||||
|
||||
# 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()
|
||||
alerts = (
|
||||
Alert.objects.filter_by_role(request.user)
|
||||
.filter(resolved=False, snoozed=False, hidden=False)
|
||||
.order_by("alert_time")[: int(request.data["top"])]
|
||||
)
|
||||
count = (
|
||||
Alert.objects.filter_by_role(request.user)
|
||||
.filter(resolved=False, snoozed=False, hidden=False)
|
||||
.count()
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"alerts_count": count,
|
||||
@@ -41,13 +44,13 @@ class GetAddAlerts(APIView):
|
||||
|
||||
elif any(
|
||||
key
|
||||
in [
|
||||
in (
|
||||
"timeFilter",
|
||||
"clientFilter",
|
||||
"severityFilter",
|
||||
"resolvedFilter",
|
||||
"snoozedFilter",
|
||||
]
|
||||
)
|
||||
for key in request.data.keys()
|
||||
):
|
||||
clientFilter = Q()
|
||||
|
||||
@@ -77,9 +77,7 @@ class TestAPIv3(TacticalTestCase):
|
||||
)
|
||||
|
||||
# add check to agent with check interval set
|
||||
check = baker.make_recipe(
|
||||
"checks.ping_check", agent=self.agent, run_interval=30
|
||||
)
|
||||
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)
|
||||
@@ -89,7 +87,7 @@ class TestAPIv3(TacticalTestCase):
|
||||
)
|
||||
|
||||
# minimum check run interval is 15 seconds
|
||||
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
|
||||
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)
|
||||
@@ -129,8 +127,15 @@ class TestAPIv3(TacticalTestCase):
|
||||
"script": script.id,
|
||||
"script_args": ["test"],
|
||||
"timeout": 30,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
},
|
||||
{
|
||||
"type": "script",
|
||||
"script": 3,
|
||||
"script_args": [],
|
||||
"timeout": 30,
|
||||
"env_vars": ["hello=world", "foo=bar"],
|
||||
},
|
||||
{"type": "script", "script": 3, "script_args": [], "timeout": 30},
|
||||
]
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -296,3 +301,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
|
||||
["this"],
|
||||
)
|
||||
|
||||
def test_get_agent_config(self):
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
url = f"/api/v3/{agent.agent_id}/config/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -19,4 +19,5 @@ urlpatterns = [
|
||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
|
||||
path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
|
||||
path("<str:agentid>/config/", views.AgentConfig.as_view()),
|
||||
]
|
||||
|
||||
25
api/tacticalrmm/apiv3/utils.py
Normal file
25
api/tacticalrmm/apiv3/utils.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import random
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from tacticalrmm.structs import AgentCheckInConfig
|
||||
|
||||
|
||||
def get_agent_config() -> AgentCheckInConfig:
|
||||
return AgentCheckInConfig(
|
||||
checkin_hello=random.randint(*getattr(settings, "CHECKIN_HELLO", (30, 60))),
|
||||
checkin_agentinfo=random.randint(
|
||||
*getattr(settings, "CHECKIN_AGENTINFO", (200, 400))
|
||||
),
|
||||
checkin_winsvc=random.randint(
|
||||
*getattr(settings, "CHECKIN_WINSVC", (2400, 3000))
|
||||
),
|
||||
checkin_pubip=random.randint(*getattr(settings, "CHECKIN_PUBIP", (300, 500))),
|
||||
checkin_disks=random.randint(*getattr(settings, "CHECKIN_DISKS", (1000, 2000))),
|
||||
checkin_sw=random.randint(*getattr(settings, "CHECKIN_SW", (2800, 3500))),
|
||||
checkin_wmi=random.randint(*getattr(settings, "CHECKIN_WMI", (3000, 4000))),
|
||||
checkin_syncmesh=random.randint(
|
||||
*getattr(settings, "CHECKIN_SYNCMESH", (800, 1200))
|
||||
),
|
||||
limit_data=getattr(settings, "LIMIT_DATA", False),
|
||||
)
|
||||
@@ -14,6 +14,7 @@ from rest_framework.views import APIView
|
||||
from accounts.models import User
|
||||
from agents.models import Agent, AgentHistory
|
||||
from agents.serializers import AgentHistorySerializer
|
||||
from apiv3.utils import get_agent_config
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
|
||||
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
|
||||
@@ -24,6 +25,7 @@ from core.utils import (
|
||||
get_core_settings,
|
||||
get_mesh_device_id,
|
||||
get_mesh_ws_url,
|
||||
get_meshagent_url,
|
||||
)
|
||||
from logs.models import DebugLog, PendingAction
|
||||
from software.models import InstalledSoftware
|
||||
@@ -39,13 +41,12 @@ from tacticalrmm.constants import (
|
||||
MeshAgentIdent,
|
||||
PAStatus,
|
||||
)
|
||||
from tacticalrmm.helpers import notify_error
|
||||
from tacticalrmm.helpers import make_random_password, notify_error
|
||||
from tacticalrmm.utils import reload_nats
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
|
||||
|
||||
class CheckIn(APIView):
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@@ -253,9 +254,7 @@ class CheckRunner(APIView):
|
||||
check.check_result.last_run
|
||||
< djangotime.now()
|
||||
- djangotime.timedelta(
|
||||
seconds=check.run_interval
|
||||
if check.run_interval
|
||||
else agent.check_interval
|
||||
seconds=check.run_interval or agent.check_interval
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -365,7 +364,6 @@ class TaskRunner(APIView):
|
||||
# check if task is a collector and update the custom field
|
||||
if task.custom_field:
|
||||
if not task_result.stderr:
|
||||
|
||||
task_result.save_collector_results()
|
||||
|
||||
status = CheckStatus.PASSING
|
||||
@@ -398,26 +396,33 @@ class MeshExe(APIView):
|
||||
def post(self, request):
|
||||
match request.data:
|
||||
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
|
||||
arch = MeshAgentIdent.WIN64
|
||||
ident = MeshAgentIdent.WIN64
|
||||
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
|
||||
arch = MeshAgentIdent.WIN32
|
||||
ident = MeshAgentIdent.WIN32
|
||||
case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | {
|
||||
"goarch": GoArch.ARM64,
|
||||
"plat": AgentPlat.DARWIN,
|
||||
}:
|
||||
ident = MeshAgentIdent.DARWIN_UNIVERSAL
|
||||
case _:
|
||||
return notify_error("Arch not specified")
|
||||
return notify_error("Arch not supported")
|
||||
|
||||
core = get_core_settings()
|
||||
|
||||
try:
|
||||
uri = get_mesh_ws_url()
|
||||
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
|
||||
mesh_device_id: str = asyncio.run(
|
||||
get_mesh_device_id(uri, core.mesh_device_group)
|
||||
)
|
||||
except:
|
||||
return notify_error("Unable to connect to mesh to get group id information")
|
||||
|
||||
if settings.DOCKER_BUILD:
|
||||
dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
|
||||
else:
|
||||
dl_url = (
|
||||
f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
|
||||
)
|
||||
dl_url = get_meshagent_url(
|
||||
ident=ident,
|
||||
plat=request.data["plat"],
|
||||
mesh_site=core.mesh_site,
|
||||
mesh_device_id=mesh_device_id,
|
||||
)
|
||||
|
||||
try:
|
||||
return download_mesh_agent(dl_url)
|
||||
@@ -452,7 +457,7 @@ class NewAgent(APIView):
|
||||
user = User.objects.create_user( # type: ignore
|
||||
username=request.data["agent_id"],
|
||||
agent=agent,
|
||||
password=User.objects.make_random_password(60), # type: ignore
|
||||
password=make_random_password(len=60),
|
||||
)
|
||||
|
||||
token = Token.objects.create(user=user)
|
||||
@@ -508,7 +513,7 @@ class Installer(APIView):
|
||||
ver = request.data["version"]
|
||||
if (
|
||||
pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
and not "-dev" in settings.LATEST_AGENT_VER
|
||||
and "-dev" not in settings.LATEST_AGENT_VER
|
||||
):
|
||||
return notify_error(
|
||||
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
|
||||
@@ -561,3 +566,12 @@ class AgentHistoryResult(APIView):
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class AgentConfig(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
ret = get_agent_config()
|
||||
return Response(ret._to_dict())
|
||||
|
||||
@@ -216,16 +216,15 @@ class Policy(BaseAuditModel):
|
||||
|
||||
@staticmethod
|
||||
def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
|
||||
|
||||
# List of all tasks to be applied
|
||||
tasks = list()
|
||||
tasks = []
|
||||
|
||||
# Get policies applied to agent and agent site and client
|
||||
policies = agent.get_agent_policies()
|
||||
|
||||
processed_policies = list()
|
||||
processed_policies = []
|
||||
|
||||
for _, policy in policies.items():
|
||||
for policy in policies.values():
|
||||
if policy and policy.active and policy.pk not in processed_policies:
|
||||
processed_policies.append(policy.pk)
|
||||
for task in policy.autotasks.all():
|
||||
@@ -235,7 +234,6 @@ class Policy(BaseAuditModel):
|
||||
|
||||
@staticmethod
|
||||
def get_policy_checks(agent: "Agent") -> "List[Check]":
|
||||
|
||||
# Get checks added to agent directly
|
||||
agent_checks = list(agent.agentchecks.all())
|
||||
|
||||
@@ -244,12 +242,12 @@ class Policy(BaseAuditModel):
|
||||
|
||||
# Used to hold the policies that will be applied and the order in which they are applied
|
||||
# Enforced policies are applied first
|
||||
enforced_checks = list()
|
||||
policy_checks = list()
|
||||
enforced_checks = []
|
||||
policy_checks = []
|
||||
|
||||
processed_policies = list()
|
||||
processed_policies = []
|
||||
|
||||
for _, policy in policies.items():
|
||||
for policy in policies.values():
|
||||
if policy and policy.active and policy.pk not in processed_policies:
|
||||
processed_policies.append(policy.pk)
|
||||
if policy.enforced:
|
||||
@@ -263,24 +261,24 @@ class Policy(BaseAuditModel):
|
||||
return []
|
||||
|
||||
# Sorted Checks already added
|
||||
added_diskspace_checks: List[str] = list()
|
||||
added_ping_checks: List[str] = list()
|
||||
added_winsvc_checks: List[str] = list()
|
||||
added_script_checks: List[int] = list()
|
||||
added_eventlog_checks: List[List[str]] = list()
|
||||
added_cpuload_checks: List[int] = list()
|
||||
added_memory_checks: List[int] = list()
|
||||
added_diskspace_checks: List[str] = []
|
||||
added_ping_checks: List[str] = []
|
||||
added_winsvc_checks: List[str] = []
|
||||
added_script_checks: List[int] = []
|
||||
added_eventlog_checks: List[List[str]] = []
|
||||
added_cpuload_checks: List[int] = []
|
||||
added_memory_checks: List[int] = []
|
||||
|
||||
# Lists all agent and policy checks that will be returned
|
||||
diskspace_checks: "List[Check]" = list()
|
||||
ping_checks: "List[Check]" = list()
|
||||
winsvc_checks: "List[Check]" = list()
|
||||
script_checks: "List[Check]" = list()
|
||||
eventlog_checks: "List[Check]" = list()
|
||||
cpuload_checks: "List[Check]" = list()
|
||||
memory_checks: "List[Check]" = list()
|
||||
diskspace_checks: "List[Check]" = []
|
||||
ping_checks: "List[Check]" = []
|
||||
winsvc_checks: "List[Check]" = []
|
||||
script_checks: "List[Check]" = []
|
||||
eventlog_checks: "List[Check]" = []
|
||||
cpuload_checks: "List[Check]" = []
|
||||
memory_checks: "List[Check]" = []
|
||||
|
||||
overridden_checks: List[int] = list()
|
||||
overridden_checks: List[int] = []
|
||||
|
||||
# Loop over checks in with enforced policies first, then non-enforced policies
|
||||
for check in enforced_checks + agent_checks + policy_checks:
|
||||
|
||||
@@ -7,5 +7,5 @@ class AutomationPolicyPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_automation_policies")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_automation_policies")
|
||||
|
||||
return _has_perm(r, "can_manage_automation_policies")
|
||||
|
||||
@@ -7,8 +7,11 @@ from rest_framework.serializers import (
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from clients.models import Client, Site
|
||||
from clients.serializers import (
|
||||
ClientMinimumSerializer,
|
||||
SiteMinimumSerializer,
|
||||
)
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
@@ -85,11 +88,29 @@ class PolicyRelatedSerializer(ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class PolicyOverviewSiteSerializer(ModelSerializer):
|
||||
workstation_policy = PolicySerializer(read_only=True)
|
||||
server_policy = PolicySerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ("pk", "name", "workstation_policy", "server_policy")
|
||||
|
||||
|
||||
class PolicyOverviewSerializer(ModelSerializer):
|
||||
sites = SerializerMethodField()
|
||||
workstation_policy = PolicySerializer(read_only=True)
|
||||
server_policy = PolicySerializer(read_only=True)
|
||||
|
||||
def get_sites(self, obj):
|
||||
return PolicyOverviewSiteSerializer(
|
||||
obj.filtered_sites,
|
||||
many=True,
|
||||
).data
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = ("pk", "name", "sites", "workstation_policy", "server_policy")
|
||||
depth = 2
|
||||
|
||||
|
||||
class PolicyCheckStatusSerializer(ModelSerializer):
|
||||
|
||||
@@ -2,8 +2,9 @@ from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from django.db.models import Prefetch
|
||||
from agents.models import Agent
|
||||
from clients.models import Site
|
||||
from core.utils import get_core_settings
|
||||
from tacticalrmm.constants import AgentMonType, TaskSyncStatus
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
@@ -86,7 +87,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
"copyId": policy.pk,
|
||||
}
|
||||
|
||||
resp = self.client.post(f"/automation/policies/", data, format="json")
|
||||
resp = self.client.post("/automation/policies/", data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
copied_policy = Policy.objects.get(name=data["name"])
|
||||
@@ -186,7 +187,17 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
baker.make("clients.Site", client=cycle(clients), _quantity=3)
|
||||
resp = self.client.get(url, format="json")
|
||||
clients = Client.objects.all()
|
||||
clients = Client.objects.select_related(
|
||||
"workstation_policy", "server_policy"
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"sites",
|
||||
queryset=Site.objects.select_related(
|
||||
"workstation_policy", "server_policy"
|
||||
),
|
||||
to_attr="filtered_sites",
|
||||
)
|
||||
)
|
||||
serializer = PolicyOverviewSerializer(clients, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -210,7 +221,6 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_policy_task_status(self):
|
||||
|
||||
# policy with a task
|
||||
policy = baker.make("automation.Policy")
|
||||
agent = baker.make_recipe("agents.agent", policy=policy)
|
||||
@@ -229,7 +239,6 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
|
||||
def test_run_win_task(self, mock_task):
|
||||
|
||||
policy = baker.make("automation.Policy")
|
||||
# create managed policy tasks
|
||||
task = baker.make_recipe("autotasks.task", policy=policy)
|
||||
@@ -272,7 +281,6 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_update_patch_policy(self):
|
||||
|
||||
# test policy doesn't exist
|
||||
resp = self.client.put("/automation/patchpolicy/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -385,7 +393,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_policy_related(self):
|
||||
|
||||
# Get Site and Client from an agent in list
|
||||
clients = baker.make("clients.Client", _quantity=5)
|
||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
|
||||
@@ -436,7 +443,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.assertEqual(len(resp.data["agents"]), 2)
|
||||
|
||||
def test_getting_agent_policy_checks(self):
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(parent=policy)
|
||||
@@ -525,7 +531,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
def test_policy_exclusions(self):
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
|
||||
@@ -7,10 +7,11 @@ from rest_framework.views import APIView
|
||||
from agents.models import Agent
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from clients.models import Client, Site
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from django.db.models import Prefetch
|
||||
|
||||
from .models import Policy
|
||||
from .permissions import AutomationPolicyPerms
|
||||
@@ -83,7 +84,6 @@ class GetUpdateDeletePolicy(APIView):
|
||||
|
||||
|
||||
class PolicyAutoTask(APIView):
|
||||
|
||||
# get status of all tasks
|
||||
def get(self, request, task):
|
||||
tasks = TaskResult.objects.filter(task=task)
|
||||
@@ -107,16 +107,24 @@ class PolicyCheck(APIView):
|
||||
|
||||
class OverviewPolicy(APIView):
|
||||
def get(self, request):
|
||||
|
||||
clients = Client.objects.filter_by_role(request.user).select_related(
|
||||
"workstation_policy", "server_policy"
|
||||
clients = (
|
||||
Client.objects.filter_by_role(request.user)
|
||||
.select_related("workstation_policy", "server_policy")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"sites",
|
||||
queryset=Site.objects.select_related(
|
||||
"workstation_policy", "server_policy"
|
||||
),
|
||||
to_attr="filtered_sites",
|
||||
)
|
||||
)
|
||||
)
|
||||
return Response(PolicyOverviewSerializer(clients, many=True).data)
|
||||
|
||||
|
||||
class GetRelated(APIView):
|
||||
def get(self, request, pk):
|
||||
|
||||
policy = (
|
||||
Policy.objects.filter(pk=pk)
|
||||
.prefetch_related(
|
||||
@@ -135,6 +143,7 @@ class GetRelated(APIView):
|
||||
|
||||
class UpdatePatchPolicy(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
# create new patch policy
|
||||
def post(self, request):
|
||||
policy = get_object_or_404(Policy, pk=request.data["policy"])
|
||||
@@ -168,7 +177,6 @@ class UpdatePatchPolicy(APIView):
|
||||
class ResetPatchPolicy(APIView):
|
||||
# bulk reset agent patch policy
|
||||
def post(self, request):
|
||||
|
||||
if "client" in request.data:
|
||||
if not _has_perm_on_client(request.user, request.data["client"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_env_vars(apps, schema_editor):
|
||||
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
|
||||
for task in AutomatedTask.objects.iterator(chunk_size=30):
|
||||
try:
|
||||
tmp = []
|
||||
if isinstance(task.actions, list) and task.actions:
|
||||
for t in task.actions:
|
||||
if isinstance(t, dict):
|
||||
if t["type"] == "script":
|
||||
try:
|
||||
t["env_vars"]
|
||||
except KeyError:
|
||||
t["env_vars"] = []
|
||||
tmp.append(t)
|
||||
if tmp:
|
||||
task.actions = tmp
|
||||
task.save(update_fields=["actions"])
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("autotasks", "0037_alter_taskresult_retcode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_env_vars),
|
||||
]
|
||||
@@ -1,9 +1,10 @@
|
||||
import asyncio
|
||||
import random
|
||||
import string
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytz
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@@ -70,7 +71,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
# format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]
|
||||
# format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": [], "env_vars": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]
|
||||
actions = JSONField(default=list)
|
||||
assigned_check = models.ForeignKey(
|
||||
"checks.Check",
|
||||
@@ -141,7 +142,6 @@ class AutomatedTask(BaseAuditModel):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
|
||||
# if task is a policy task clear cache on everything
|
||||
if self.policy:
|
||||
cache.delete_many_pattern("site_*_tasks")
|
||||
@@ -167,7 +167,6 @@ class AutomatedTask(BaseAuditModel):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# if task is a policy task clear cache on everything
|
||||
if self.policy:
|
||||
cache.delete_many_pattern("site_*_tasks")
|
||||
@@ -225,15 +224,13 @@ class AutomatedTask(BaseAuditModel):
|
||||
def create_policy_task(
|
||||
self, policy: "Policy", assigned_check: "Optional[Check]" = None
|
||||
) -> None:
|
||||
### Copies certain properties on this task (self) to a new task and sets it to the supplied Policy
|
||||
fields_to_copy = POLICY_TASK_FIELDS_TO_COPY
|
||||
|
||||
# Copies certain properties on this task (self) to a new task and sets it to the supplied Policy
|
||||
task = AutomatedTask.objects.create(
|
||||
policy=policy,
|
||||
assigned_check=assigned_check,
|
||||
)
|
||||
|
||||
for field in fields_to_copy:
|
||||
for field in POLICY_TASK_FIELDS_TO_COPY:
|
||||
setattr(task, field, getattr(self, field))
|
||||
|
||||
task.save()
|
||||
@@ -251,9 +248,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
"trigger": self.task_type
|
||||
if self.task_type != TaskType.CHECK_FAILURE
|
||||
else TaskType.MANUAL,
|
||||
"multiple_instances": self.task_instance_policy
|
||||
if self.task_instance_policy
|
||||
else 0,
|
||||
"multiple_instances": self.task_instance_policy or 0,
|
||||
"delete_expired_task_after": self.remove_if_not_scheduled
|
||||
if self.expire_date
|
||||
else False,
|
||||
@@ -262,25 +257,25 @@ class AutomatedTask(BaseAuditModel):
|
||||
else True,
|
||||
}
|
||||
|
||||
if self.task_type in [
|
||||
if self.task_type in (
|
||||
TaskType.RUN_ONCE,
|
||||
TaskType.DAILY,
|
||||
TaskType.WEEKLY,
|
||||
TaskType.MONTHLY,
|
||||
TaskType.MONTHLY_DOW,
|
||||
]:
|
||||
):
|
||||
# set runonce task in future if creating and run_asap_after_missed is set
|
||||
if (
|
||||
not editing
|
||||
and self.task_type == TaskType.RUN_ONCE
|
||||
and self.run_asap_after_missed
|
||||
and agent
|
||||
and self.run_time_date
|
||||
< djangotime.now().astimezone(pytz.timezone(agent.timezone))
|
||||
and self.run_time_date.replace(tzinfo=ZoneInfo(agent.timezone))
|
||||
< djangotime.now().astimezone(ZoneInfo(agent.timezone))
|
||||
):
|
||||
self.run_time_date = (
|
||||
djangotime.now() + djangotime.timedelta(minutes=5)
|
||||
).astimezone(pytz.timezone(agent.timezone))
|
||||
).astimezone(ZoneInfo(agent.timezone))
|
||||
|
||||
task["start_year"] = int(self.run_time_date.strftime("%Y"))
|
||||
task["start_month"] = int(self.run_time_date.strftime("%-m"))
|
||||
@@ -315,7 +310,6 @@ class AutomatedTask(BaseAuditModel):
|
||||
task["days_of_week"] = self.run_time_bit_weekdays
|
||||
|
||||
elif self.task_type == TaskType.MONTHLY:
|
||||
|
||||
# check if "last day is configured"
|
||||
if self.monthly_days_of_month >= 0x80000000:
|
||||
task["days_of_month"] = self.monthly_days_of_month - 0x80000000
|
||||
@@ -432,10 +426,8 @@ class AutomatedTask(BaseAuditModel):
|
||||
if r != "ok" and "The system cannot find the file specified" not in r:
|
||||
task_result.sync_status = TaskSyncStatus.PENDING_DELETION
|
||||
|
||||
try:
|
||||
with suppress(DatabaseError):
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
except DatabaseError:
|
||||
pass
|
||||
|
||||
DebugLog.warning(
|
||||
agent=agent,
|
||||
@@ -532,7 +524,6 @@ class TaskResult(models.Model):
|
||||
)
|
||||
|
||||
def save_collector_results(self) -> None:
|
||||
|
||||
agent_field = self.task.custom_field.get_or_create_field_value(self.agent)
|
||||
|
||||
value = (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework import serializers
|
||||
|
||||
from scripts.models import Script
|
||||
@@ -14,7 +17,6 @@ class TaskResultSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
|
||||
check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc")
|
||||
schedule = serializers.ReadOnlyField()
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
@@ -30,52 +32,49 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
def validate_actions(self, value):
|
||||
|
||||
if not value:
|
||||
raise serializers.ValidationError(
|
||||
f"There must be at least one action configured"
|
||||
"There must be at least one action configured"
|
||||
)
|
||||
|
||||
for action in value:
|
||||
if "type" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"Each action must have a type field of either 'script' or 'cmd'"
|
||||
"Each action must have a type field of either 'script' or 'cmd'"
|
||||
)
|
||||
|
||||
if action["type"] == "script":
|
||||
if "script" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"A script action type must have a 'script' field with primary key of script"
|
||||
"A script action type must have a 'script' field with primary key of script"
|
||||
)
|
||||
|
||||
if "script_args" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"A script action type must have a 'script_args' field with an array of arguments"
|
||||
"A script action type must have a 'script_args' field with an array of arguments"
|
||||
)
|
||||
|
||||
if "timeout" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"A script action type must have a 'timeout' field"
|
||||
"A script action type must have a 'timeout' field"
|
||||
)
|
||||
|
||||
if action["type"] == "cmd":
|
||||
if "command" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"A command action type must have a 'command' field"
|
||||
"A command action type must have a 'command' field"
|
||||
)
|
||||
|
||||
if "timeout" not in action:
|
||||
raise serializers.ValidationError(
|
||||
f"A command action type must have a 'timeout' field"
|
||||
"A command action type must have a 'timeout' field"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# allow editing with task_type not specified
|
||||
if self.instance and "task_type" not in data:
|
||||
|
||||
# remove schedule related fields from data
|
||||
if "run_time_date" in data:
|
||||
del data["run_time_date"]
|
||||
@@ -97,16 +96,23 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
del data["assigned_check"]
|
||||
return data
|
||||
|
||||
if (
|
||||
"expire_date" in data
|
||||
and isinstance(data["expire_date"], datetime)
|
||||
and djangotime.now() > data["expire_date"]
|
||||
):
|
||||
raise serializers.ValidationError("Expires date/time is in the past")
|
||||
|
||||
# run_time_date required
|
||||
if (
|
||||
data["task_type"]
|
||||
in [
|
||||
in (
|
||||
TaskType.RUN_ONCE,
|
||||
TaskType.DAILY,
|
||||
TaskType.WEEKLY,
|
||||
TaskType.MONTHLY,
|
||||
TaskType.MONTHLY_DOW,
|
||||
]
|
||||
)
|
||||
and not data["run_time_date"]
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -180,7 +186,6 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
|
||||
if obj.agent:
|
||||
alert_template = obj.agent.alert_template
|
||||
else:
|
||||
@@ -188,13 +193,12 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.task_always_email,
|
||||
"always_text": alert_template.task_always_text,
|
||||
"always_alert": alert_template.task_always_alert,
|
||||
}
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.task_always_email,
|
||||
"always_text": alert_template.task_always_text,
|
||||
"always_alert": alert_template.task_always_alert,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
@@ -229,6 +233,12 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
# script doesn't exist so remove it
|
||||
actions_to_remove.append(action["script"])
|
||||
continue
|
||||
# wrote a custom migration for env_vars but leaving this just in case.
|
||||
# can be removed later
|
||||
try:
|
||||
env_vars = action["env_vars"]
|
||||
except KeyError:
|
||||
env_vars = []
|
||||
tmp.append(
|
||||
{
|
||||
"type": "script",
|
||||
@@ -241,6 +251,12 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
),
|
||||
"shell": script.shell,
|
||||
"timeout": action["timeout"],
|
||||
"run_as_user": script.run_as_user,
|
||||
"env_vars": Script.parse_script_env_vars(
|
||||
agent=agent,
|
||||
shell=script.shell,
|
||||
env_vars=env_vars,
|
||||
),
|
||||
}
|
||||
)
|
||||
if actions_to_remove:
|
||||
|
||||
@@ -1,134 +1,163 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
from collections import namedtuple
|
||||
from contextlib import suppress
|
||||
from time import sleep
|
||||
from typing import Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import msgpack
|
||||
import nats
|
||||
from django.utils import timezone as djangotime
|
||||
from nats.errors import TimeoutError
|
||||
|
||||
from agents.models import Agent
|
||||
from alerts.models import Alert
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from logs.models import DebugLog
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.constants import DebugLogType
|
||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ORPHANED_WIN_TASK_LOCK
|
||||
from tacticalrmm.helpers import rand_range, setup_nats_options
|
||||
from tacticalrmm.utils import redis_lock
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nats.aio.client import Client as NATSClient
|
||||
|
||||
|
||||
@app.task
|
||||
def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
|
||||
try:
|
||||
with suppress(
|
||||
AutomatedTask.DoesNotExist,
|
||||
Agent.DoesNotExist,
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if agent_id:
|
||||
task.create_task_on_agent(Agent.objects.get(agent_id=agent_id))
|
||||
else:
|
||||
task.create_task_on_agent()
|
||||
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
|
||||
pass
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str:
|
||||
try:
|
||||
with suppress(
|
||||
AutomatedTask.DoesNotExist,
|
||||
Agent.DoesNotExist,
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if agent_id:
|
||||
task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id))
|
||||
else:
|
||||
task.modify_task_on_agent()
|
||||
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
|
||||
pass
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
|
||||
try:
|
||||
with suppress(
|
||||
AutomatedTask.DoesNotExist,
|
||||
Agent.DoesNotExist,
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if agent_id:
|
||||
task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id))
|
||||
else:
|
||||
task.delete_task_on_agent()
|
||||
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
|
||||
pass
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def run_win_task(pk: int, agent_id: Optional[str] = None) -> str:
|
||||
try:
|
||||
with suppress(
|
||||
AutomatedTask.DoesNotExist,
|
||||
Agent.DoesNotExist,
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if agent_id:
|
||||
task.run_win_task(Agent.objects.get(agent_id=agent_id))
|
||||
else:
|
||||
task.run_win_task()
|
||||
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
|
||||
pass
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def remove_orphaned_win_tasks() -> None:
|
||||
from agents.models import Agent
|
||||
@app.task(bind=True)
|
||||
def remove_orphaned_win_tasks(self) -> str:
|
||||
with redis_lock(ORPHANED_WIN_TASK_LOCK, self.app.oid) as acquired:
|
||||
if not acquired:
|
||||
return f"{self.app.oid} still running"
|
||||
|
||||
for agent in Agent.online_agents():
|
||||
r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
|
||||
from core.tasks import _get_agent_qs
|
||||
|
||||
if not isinstance(r, list): # empty list
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Unable to pull list of scheduled tasks on {agent.hostname}: {r}",
|
||||
)
|
||||
continue
|
||||
AgentTup = namedtuple("AgentTup", ["agent_id", "task_names"])
|
||||
items: "list[AgentTup]" = []
|
||||
exclude_tasks = ("TacticalRMM_SchedReboot",)
|
||||
|
||||
agent_task_names = [
|
||||
task.win_task_name for task in agent.get_tasks_with_policies()
|
||||
]
|
||||
for agent in _get_agent_qs():
|
||||
if agent.status == AGENT_STATUS_ONLINE:
|
||||
names = [task.win_task_name for task in agent.get_tasks_with_policies()]
|
||||
items.append(AgentTup._make([agent.agent_id, names]))
|
||||
|
||||
exclude_tasks = (
|
||||
"TacticalRMM_fixmesh",
|
||||
"TacticalRMM_SchedReboot",
|
||||
"TacticalRMM_sync",
|
||||
"TacticalRMM_agentupdate",
|
||||
)
|
||||
async def _handle_task(nc: "NATSClient", sub, data, names) -> str:
|
||||
try:
|
||||
msg = await nc.request(
|
||||
subject=sub, payload=msgpack.dumps(data), timeout=5
|
||||
)
|
||||
except TimeoutError:
|
||||
return "timeout"
|
||||
|
||||
for task in r:
|
||||
if task.startswith(exclude_tasks):
|
||||
# skip system tasks or any pending reboots
|
||||
continue
|
||||
try:
|
||||
r = msgpack.loads(msg.data)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
if task.startswith("TacticalRMM_") and task not in agent_task_names:
|
||||
# delete task since it doesn't exist in UI
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": task},
|
||||
}
|
||||
ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
|
||||
if ret != "ok":
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
|
||||
)
|
||||
else:
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Removed orphaned task {task} from {agent.hostname}",
|
||||
)
|
||||
if not isinstance(r, list):
|
||||
return "notlist"
|
||||
|
||||
for name in r:
|
||||
if name.startswith(exclude_tasks):
|
||||
# skip system tasks or any pending reboots
|
||||
continue
|
||||
|
||||
if name.startswith("TacticalRMM_") and name not in names:
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": name},
|
||||
}
|
||||
print(f"Deleting orphaned task: {name} on agent {sub}")
|
||||
await nc.publish(subject=sub, payload=msgpack.dumps(nats_data))
|
||||
|
||||
return "ok"
|
||||
|
||||
async def _run() -> None:
|
||||
opts = setup_nats_options()
|
||||
try:
|
||||
nc = await nats.connect(**opts)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
payload = {"func": "listschedtasks"}
|
||||
tasks = [
|
||||
_handle_task(
|
||||
nc=nc, sub=item.agent_id, data=payload, names=item.task_names
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
await nc.flush()
|
||||
await nc.close()
|
||||
|
||||
asyncio.run(_run())
|
||||
return "completed"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -139,7 +168,7 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -151,7 +180,7 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -161,7 +190,6 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
|
||||
|
||||
@app.task
|
||||
def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -172,7 +200,7 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -184,7 +212,7 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -194,7 +222,6 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
|
||||
|
||||
@app.task
|
||||
def handle_resolved_task_sms_alert(pk: int) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -205,7 +232,7 @@ def handle_resolved_task_sms_alert(pk: int) -> str:
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_resolved_sms()
|
||||
alert.resolved_sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_sms_sent"])
|
||||
@@ -215,7 +242,6 @@ def handle_resolved_task_sms_alert(pk: int) -> str:
|
||||
|
||||
@app.task
|
||||
def handle_resolved_task_email_alert(pk: int) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -226,7 +252,7 @@ def handle_resolved_task_email_alert(pk: int) -> str:
|
||||
task_result = TaskResult.objects.get(
|
||||
task=alert.assigned_task, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
task_result.send_resolved_email()
|
||||
alert.resolved_email_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_email_sent"])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
@@ -8,7 +8,7 @@ from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import AutomatedTask, TaskResult, TaskSyncStatus
|
||||
from .serializers import TaskSerializer
|
||||
from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task
|
||||
from .tasks import create_win_task_schedule, run_win_task
|
||||
|
||||
base_url = "/tasks"
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
# setup data
|
||||
script = baker.make_recipe("scripts.script")
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
policy = baker.make("automation.Policy") # noqa
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
custom_field = baker.make("core.CustomField")
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"weekly_interval": 2,
|
||||
"run_time_bit_weekdays": 26,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -160,7 +160,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"monthly_months_of_year": 56,
|
||||
"monthly_days_of_month": 350,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -183,7 +183,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"monthly_weeks_of_month": 4,
|
||||
"run_time_bit_weekdays": 15,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -206,7 +206,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"monthly_weeks_of_month": 4,
|
||||
"run_time_bit_weekdays": 15,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -238,7 +238,6 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_get_autotask(self):
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
@@ -258,7 +257,9 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent_task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
policy = baker.make("automation.Policy")
|
||||
policy_task = baker.make("autotasks.AutomatedTask", enabled=True, policy=policy)
|
||||
policy_task = baker.make( # noqa
|
||||
"autotasks.AutomatedTask", enabled=True, policy=policy
|
||||
)
|
||||
custom_field = baker.make("core.CustomField")
|
||||
script = baker.make("scripts.Script")
|
||||
|
||||
@@ -294,7 +295,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"monthly_weeks_of_month": 4,
|
||||
"run_time_bit_weekdays": 15,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -314,7 +315,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"monthly_weeks_of_month": 4,
|
||||
"run_time_bit_weekdays": 15,
|
||||
"run_time_date": djangotime.now(),
|
||||
"expire_date": djangotime.now(),
|
||||
"expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
|
||||
"repetition_interval": "30S",
|
||||
"repetition_duration": "1H",
|
||||
"random_task_delay": "5M",
|
||||
@@ -381,60 +382,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_remove_orphaned_win_task(self, nats_cmd):
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
baker.make_recipe("agents.offline_agent")
|
||||
task1 = AutomatedTask.objects.create(
|
||||
agent=agent,
|
||||
name="test task 1",
|
||||
)
|
||||
|
||||
# test removing an orphaned task
|
||||
win_tasks = [
|
||||
"Adobe Acrobat Update Task",
|
||||
"AdobeGCInvoker-1.0",
|
||||
"GoogleUpdateTaskMachineCore",
|
||||
"GoogleUpdateTaskMachineUA",
|
||||
"OneDrive Standalone Update Task-S-1-5-21-717461175-241712648-1206041384-1001",
|
||||
task1.win_task_name,
|
||||
"TacticalRMM_fixmesh",
|
||||
"TacticalRMM_SchedReboot_jk324kajd",
|
||||
"TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb", # orphaned task
|
||||
]
|
||||
|
||||
calls = [
|
||||
call({"func": "listschedtasks"}, timeout=10),
|
||||
call(
|
||||
{
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {
|
||||
"name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
),
|
||||
]
|
||||
|
||||
nats_cmd.side_effect = [win_tasks, "ok"]
|
||||
remove_orphaned_win_tasks()
|
||||
self.assertEqual(nats_cmd.call_count, 2)
|
||||
nats_cmd.assert_has_calls(calls)
|
||||
|
||||
# test nats delete task fail
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.side_effect = [win_tasks, "error deleting task"]
|
||||
remove_orphaned_win_tasks()
|
||||
nats_cmd.assert_has_calls(calls)
|
||||
self.assertEqual(nats_cmd.call_count, 2)
|
||||
|
||||
# no orphaned tasks
|
||||
nats_cmd.reset_mock()
|
||||
win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb")
|
||||
nats_cmd.side_effect = [win_tasks, "ok"]
|
||||
remove_orphaned_win_tasks()
|
||||
self.assertEqual(nats_cmd.call_count, 1)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_win_task(self, nats_cmd):
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
@@ -766,12 +713,14 @@ class TestTaskPermissions(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5)
|
||||
unauthorized_task = baker.make(
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5) # noqa
|
||||
unauthorized_task = baker.make( # noqa
|
||||
"autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7
|
||||
)
|
||||
|
||||
policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2)
|
||||
policy_tasks = baker.make( # noqa
|
||||
"autotasks.AutomatedTask", policy=policy, _quantity=2
|
||||
)
|
||||
|
||||
# test super user access
|
||||
self.check_authorized_superuser("get", f"{base_url}/")
|
||||
@@ -864,7 +813,7 @@ class TestTaskPermissions(TacticalTestCase):
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
for data in [policy_data, agent_data]:
|
||||
for data in (policy_data, agent_data):
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
@@ -900,8 +849,7 @@ class TestTaskPermissions(TacticalTestCase):
|
||||
)
|
||||
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
|
||||
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
for method in ("get", "put", "delete"):
|
||||
url = f"{base_url}/{task.id}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_task.id}/"
|
||||
policy_url = f"{base_url}/{policy_task.id}/"
|
||||
@@ -939,7 +887,6 @@ class TestTaskPermissions(TacticalTestCase):
|
||||
self.check_authorized(method, policy_url)
|
||||
|
||||
def test_task_action_permissions(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
|
||||
@@ -18,7 +18,6 @@ class GetAddAutoTasks(APIView):
|
||||
permission_classes = [IsAuthenticated, AutoTaskPerms]
|
||||
|
||||
def get(self, request, agent_id=None, policy=None):
|
||||
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
tasks = agent.get_tasks_with_policies()
|
||||
@@ -59,7 +58,6 @@ class GetEditDeleteAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, AutoTaskPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
@@ -68,7 +66,6 @@ class GetEditDeleteAutoTask(APIView):
|
||||
return Response(TaskSerializer(task).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
|
||||
0
api/tacticalrmm/beta/v1/__init__.py
Normal file
0
api/tacticalrmm/beta/v1/__init__.py
Normal file
0
api/tacticalrmm/beta/v1/agent/__init__.py
Normal file
0
api/tacticalrmm/beta/v1/agent/__init__.py
Normal file
37
api/tacticalrmm/beta/v1/agent/filter.py
Normal file
37
api/tacticalrmm/beta/v1/agent/filter.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import django_filters
|
||||
from agents.models import Agent
|
||||
|
||||
|
||||
class AgentFilter(django_filters.FilterSet):
|
||||
last_seen_range = django_filters.DateTimeFromToRangeFilter(field_name="last_seen")
|
||||
total_ram_range = django_filters.NumericRangeFilter(field_name="total_ram")
|
||||
patches_last_installed_range = django_filters.DateTimeFromToRangeFilter(
|
||||
field_name="patches_last_installed"
|
||||
)
|
||||
|
||||
client_id = django_filters.NumberFilter(method="client_id_filter")
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"id",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"operating_system",
|
||||
"plat",
|
||||
"monitoring_type",
|
||||
"needs_reboot",
|
||||
"logged_in_username",
|
||||
"last_logged_in_user",
|
||||
"alert_template",
|
||||
"site",
|
||||
"policy",
|
||||
"last_seen_range",
|
||||
"total_ram_range",
|
||||
"patches_last_installed_range",
|
||||
]
|
||||
|
||||
def client_id_filter(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(site__client__id=value)
|
||||
return queryset
|
||||
40
api/tacticalrmm/beta/v1/agent/views.py
Normal file
40
api/tacticalrmm/beta/v1/agent/views.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.permissions import AgentPerms
|
||||
from beta.v1.agent.filter import AgentFilter
|
||||
from beta.v1.pagination import StandardResultsSetPagination
|
||||
from ..serializers import DetailAgentSerializer, ListAgentSerializer
|
||||
|
||||
|
||||
class AgentViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
queryset = Agent.objects.all()
|
||||
pagination_class = StandardResultsSetPagination
|
||||
http_method_names = ["get", "put"]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = AgentFilter
|
||||
search_fields = ["hostname", "services"]
|
||||
ordering_fields = ["id"]
|
||||
ordering = ["id"]
|
||||
|
||||
def check_permissions(self, request: Request) -> None:
|
||||
if "agent_id" in request.query_params:
|
||||
self.kwargs["agent_id"] = request.query_params["agent_id"]
|
||||
super().check_permissions(request)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method == "POST":
|
||||
self.permission_classes = [IsAuthenticated]
|
||||
return super().get_permissions()
|
||||
|
||||
def get_serializer_class(self) -> type[BaseSerializer]:
|
||||
if self.kwargs:
|
||||
if self.kwargs["pk"]:
|
||||
return DetailAgentSerializer
|
||||
return ListAgentSerializer
|
||||
0
api/tacticalrmm/beta/v1/client/__init__.py
Normal file
0
api/tacticalrmm/beta/v1/client/__init__.py
Normal file
13
api/tacticalrmm/beta/v1/client/views.py
Normal file
13
api/tacticalrmm/beta/v1/client/views.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from clients.models import Client
|
||||
from clients.permissions import ClientsPerms
|
||||
from ..serializers import ClientSerializer
|
||||
|
||||
|
||||
class ClientViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, ClientsPerms]
|
||||
queryset = Client.objects.all()
|
||||
serializer_class = ClientSerializer
|
||||
http_method_names = ["get", "put"]
|
||||
7
api/tacticalrmm/beta/v1/pagination.py
Normal file
7
api/tacticalrmm/beta/v1/pagination.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 100
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
||||
73
api/tacticalrmm/beta/v1/serializers.py
Normal file
73
api/tacticalrmm/beta/v1/serializers.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from agents.models import Agent
|
||||
from clients.models import Client, Site
|
||||
|
||||
|
||||
class ListAgentSerializer(serializers.ModelSerializer[Agent]):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class DetailAgentSerializer(serializers.ModelSerializer[Agent]):
|
||||
status = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = (
|
||||
"version",
|
||||
"operating_system",
|
||||
"plat",
|
||||
"goarch",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"last_seen",
|
||||
"services",
|
||||
"public_ip",
|
||||
"total_ram",
|
||||
"disks",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
"last_logged_in_user",
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"mesh_node_id",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
"check_interval",
|
||||
"needs_reboot",
|
||||
"choco_installed",
|
||||
"wmi_detail",
|
||||
"patches_last_installed",
|
||||
"time_zone",
|
||||
"maintenance_mode",
|
||||
"block_policy_inheritance",
|
||||
"alert_template",
|
||||
"site",
|
||||
"policy",
|
||||
"status",
|
||||
"checks",
|
||||
"pending_actions_count",
|
||||
"cpu_model",
|
||||
"graphics",
|
||||
"local_ips",
|
||||
"make_model",
|
||||
"physical_disks",
|
||||
"serial_number",
|
||||
)
|
||||
|
||||
|
||||
class ClientSerializer(serializers.ModelSerializer[Client]):
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SiteSerializer(serializers.ModelSerializer[Site]):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = "__all__"
|
||||
21
api/tacticalrmm/beta/v1/site/views.py
Normal file
21
api/tacticalrmm/beta/v1/site/views.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from clients.models import Site
|
||||
from clients.permissions import SitesPerms
|
||||
from beta.v1.pagination import StandardResultsSetPagination
|
||||
from ..serializers import SiteSerializer
|
||||
|
||||
|
||||
class SiteViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, SitesPerms]
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = SiteSerializer
|
||||
pagination_class = StandardResultsSetPagination
|
||||
http_method_names = ["get", "put"]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
search_fields = ["name"]
|
||||
ordering_fields = ["id"]
|
||||
ordering = ["id"]
|
||||
12
api/tacticalrmm/beta/v1/urls.py
Normal file
12
api/tacticalrmm/beta/v1/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import routers
|
||||
from .agent import views as agent
|
||||
from .client import views as client
|
||||
from .site import views as site
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
router.register("agent", agent.AgentViewSet, basename="agent")
|
||||
router.register("client", client.ClientViewSet, basename="client")
|
||||
router.register("site", site.SiteViewSet, basename="site")
|
||||
|
||||
urlpatterns = router.urls
|
||||
25
api/tacticalrmm/checks/migrations/0031_check_env_vars.py
Normal file
25
api/tacticalrmm/checks/migrations/0031_check_env_vars.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-03 09:38
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("checks", "0030_alter_checkresult_retcode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="check",
|
||||
name="env_vars",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(blank=True, null=True),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -98,6 +98,12 @@ class Check(BaseAuditModel):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
env_vars = ArrayField(
|
||||
models.TextField(null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
info_return_codes = ArrayField(
|
||||
models.PositiveIntegerField(),
|
||||
null=True,
|
||||
@@ -149,11 +155,10 @@ class Check(BaseAuditModel):
|
||||
def __str__(self):
|
||||
if self.agent:
|
||||
return f"{self.agent.hostname} - {self.readable_desc}"
|
||||
else:
|
||||
return f"{self.policy.name} - {self.readable_desc}"
|
||||
|
||||
return f"{self.policy.name} - {self.readable_desc}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# if check is a policy check clear cache on everything
|
||||
if self.policy:
|
||||
cache.delete_many_pattern("site_*_checks")
|
||||
@@ -169,7 +174,6 @@ class Check(BaseAuditModel):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# if check is a policy check clear cache on everything
|
||||
if self.policy:
|
||||
cache.delete_many_pattern("site_*_checks")
|
||||
@@ -188,7 +192,6 @@ class Check(BaseAuditModel):
|
||||
def readable_desc(self):
|
||||
display = self.get_check_type_display() # type: ignore
|
||||
if self.check_type == CheckType.DISK_SPACE:
|
||||
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
@@ -198,10 +201,7 @@ class Check(BaseAuditModel):
|
||||
return f"{display}: Drive {self.disk} - {text}"
|
||||
elif self.check_type == CheckType.PING:
|
||||
return f"{display}: {self.name}"
|
||||
elif (
|
||||
self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
|
||||
):
|
||||
|
||||
elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
@@ -215,17 +215,14 @@ class Check(BaseAuditModel):
|
||||
return f"{display}: {self.name}"
|
||||
elif self.check_type == CheckType.SCRIPT:
|
||||
return f"{display}: {self.script.name}"
|
||||
else:
|
||||
return "n/a"
|
||||
|
||||
return "n/a"
|
||||
|
||||
@staticmethod
|
||||
def non_editable_fields() -> list[str]:
|
||||
return CHECKS_NON_EDITABLE_FIELDS
|
||||
|
||||
def create_policy_check(self, policy: "Policy") -> None:
|
||||
|
||||
fields_to_copy = POLICY_CHECK_FIELDS_TO_COPY
|
||||
|
||||
check = Check.objects.create(
|
||||
policy=policy,
|
||||
)
|
||||
@@ -233,13 +230,12 @@ class Check(BaseAuditModel):
|
||||
for task in self.assignedtasks.all(): # type: ignore
|
||||
task.create_policy_task(policy=policy, assigned_check=check)
|
||||
|
||||
for field in fields_to_copy:
|
||||
for field in POLICY_CHECK_FIELDS_TO_COPY:
|
||||
setattr(check, field, getattr(self, field))
|
||||
|
||||
check.save()
|
||||
|
||||
def should_create_alert(self, alert_template=None):
|
||||
|
||||
return (
|
||||
self.dashboard_alert
|
||||
or self.email_alert
|
||||
@@ -333,14 +329,13 @@ class CheckResult(models.Model):
|
||||
return f"{self.agent.hostname} - {self.assigned_check}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# if check is a policy check clear cache on everything
|
||||
if not self.alert_severity and self.assigned_check.check_type in [
|
||||
if not self.alert_severity and self.assigned_check.check_type in (
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]:
|
||||
):
|
||||
self.alert_severity = AlertSeverity.WARNING
|
||||
|
||||
super(CheckResult, self).save(
|
||||
@@ -371,7 +366,6 @@ class CheckResult(models.Model):
|
||||
update_fields = []
|
||||
# cpuload or mem checks
|
||||
if check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
|
||||
self.history.append(data["percent"])
|
||||
|
||||
if len(self.history) > 15:
|
||||
@@ -536,7 +530,6 @@ class CheckResult(models.Model):
|
||||
return self.status
|
||||
|
||||
def send_email(self):
|
||||
|
||||
CORE = get_core_settings()
|
||||
|
||||
body: str = ""
|
||||
@@ -565,14 +558,12 @@ class CheckResult(models.Model):
|
||||
body = subject + f" - Disk {self.assigned_check.disk} does not exist"
|
||||
|
||||
elif self.assigned_check.check_type == CheckType.SCRIPT:
|
||||
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
elif self.assigned_check.check_type == CheckType.PING:
|
||||
|
||||
body = self.more_info
|
||||
|
||||
elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
@@ -594,7 +585,6 @@ class CheckResult(models.Model):
|
||||
body = subject + f" - Status: {self.more_info}"
|
||||
|
||||
elif self.assigned_check.check_type == CheckType.EVENT_LOG:
|
||||
|
||||
if self.assigned_check.event_source and self.assigned_check.event_message:
|
||||
start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source}, containing string {self.assigned_check.event_message} "
|
||||
elif self.assigned_check.event_source:
|
||||
@@ -616,7 +606,6 @@ class CheckResult(models.Model):
|
||||
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
CORE = get_core_settings()
|
||||
body: str = ""
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
from tacticalrmm.permissions import (
|
||||
_has_perm,
|
||||
_has_perm_on_agent,
|
||||
_has_perm_on_client,
|
||||
_has_perm_on_site,
|
||||
)
|
||||
|
||||
|
||||
class ChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
if r.method in ("GET", "PATCH"):
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
@@ -21,3 +26,17 @@ class RunChecksPerms(permissions.BasePermission):
|
||||
return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class BulkRunChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if not _has_perm(r, "can_run_checks"):
|
||||
return False
|
||||
|
||||
if view.kwargs["target"] == "client":
|
||||
return _has_perm_on_client(user=r.user, client_id=view.kwargs["pk"])
|
||||
|
||||
elif view.kwargs["target"] == "site":
|
||||
return _has_perm_on_site(user=r.user, site_id=view.kwargs["pk"])
|
||||
|
||||
return False
|
||||
|
||||
@@ -22,7 +22,6 @@ class CheckResultSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
readable_desc = serializers.ReadOnlyField()
|
||||
assignedtasks = AssignedTaskField(many=True, read_only=True)
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
@@ -43,13 +42,13 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.check_always_email,
|
||||
"always_text": alert_template.check_always_text,
|
||||
"always_alert": alert_template.check_always_alert,
|
||||
}
|
||||
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.check_always_email,
|
||||
"always_text": alert_template.check_always_text,
|
||||
"always_alert": alert_template.check_always_alert,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
@@ -82,7 +81,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -91,7 +90,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be greater than Error Threshold"
|
||||
"Warning threshold must be greater than Error Threshold"
|
||||
)
|
||||
|
||||
# ping checks
|
||||
@@ -113,7 +112,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -122,7 +121,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be less than Error Threshold"
|
||||
"Warning threshold must be less than Error Threshold"
|
||||
)
|
||||
|
||||
if check_type == CheckType.MEMORY and not self.instance:
|
||||
@@ -133,7 +132,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -142,7 +141,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be less than Error Threshold"
|
||||
"Warning threshold must be less than Error Threshold"
|
||||
)
|
||||
|
||||
return val
|
||||
@@ -158,6 +157,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
# only send data needed for agent to run a check
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
script_args = serializers.SerializerMethodField()
|
||||
env_vars = serializers.SerializerMethodField()
|
||||
|
||||
def get_script_args(self, obj):
|
||||
if obj.check_type != CheckType.SCRIPT:
|
||||
@@ -168,6 +168,19 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
agent=agent, shell=obj.script.shell, args=obj.script_args
|
||||
)
|
||||
|
||||
def get_env_vars(self, obj):
|
||||
if obj.check_type != CheckType.SCRIPT:
|
||||
return []
|
||||
|
||||
agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent
|
||||
|
||||
return Script.parse_script_env_vars(
|
||||
agent=agent,
|
||||
shell=obj.script.shell,
|
||||
env_vars=obj.env_vars
|
||||
or obj.script.env_vars, # check's env_vars override the script's env vars
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
exclude = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import datetime as dt
|
||||
import random
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
|
||||
@@ -8,6 +7,7 @@ from django.utils import timezone as djangotime
|
||||
from alerts.models import Alert
|
||||
from checks.models import CheckResult
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.helpers import rand_range
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -24,7 +24,7 @@ def handle_check_email_alert_task(
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -36,7 +36,7 @@ def handle_check_email_alert_task(
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
@@ -46,7 +46,6 @@ def handle_check_email_alert_task(
|
||||
|
||||
@app.task
|
||||
def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -57,7 +56,7 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -69,7 +68,7 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
@@ -79,7 +78,6 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
|
||||
|
||||
@app.task
|
||||
def handle_resolved_check_sms_alert_task(pk: int) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -90,7 +88,7 @@ def handle_resolved_check_sms_alert_task(pk: int) -> str:
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 3))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_resolved_sms()
|
||||
alert.resolved_sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_sms_sent"])
|
||||
@@ -100,7 +98,6 @@ def handle_resolved_check_sms_alert_task(pk: int) -> str:
|
||||
|
||||
@app.task
|
||||
def handle_resolved_check_email_alert_task(pk: int) -> str:
|
||||
|
||||
try:
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
except Alert.DoesNotExist:
|
||||
@@ -111,7 +108,7 @@ def handle_resolved_check_email_alert_task(pk: int) -> str:
|
||||
check_result = CheckResult.objects.get(
|
||||
assigned_check=alert.assigned_check, agent=alert.agent
|
||||
)
|
||||
sleep(random.randint(1, 5))
|
||||
sleep(rand_range(100, 1500))
|
||||
check_result.send_resolved_email()
|
||||
alert.resolved_email_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_email_sent"])
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
self.assertEqual(len(resp.data), 4)
|
||||
|
||||
# test agent doesn't exist
|
||||
url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
|
||||
url = "/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
@@ -101,8 +101,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
for payload in (agent_payload, policy_payload):
|
||||
# add valid check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -148,8 +147,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
"fails_b4_alert": 9,
|
||||
}
|
||||
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
for payload in (agent_payload, policy_payload):
|
||||
# add cpu check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -174,6 +172,31 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_reset_all_checks_status(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
baker.make("checks.CheckResult", assigned_check=check, agent=agent)
|
||||
baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_id=check.id,
|
||||
agent_id=agent.agent_id,
|
||||
_quantity=30,
|
||||
)
|
||||
baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_id=check.id,
|
||||
agent_id=agent.agent_id,
|
||||
_quantity=30,
|
||||
)
|
||||
|
||||
url = f"{base_url}/{agent.agent_id}/resetall/"
|
||||
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_memory_check(self):
|
||||
url = f"{base_url}/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -195,8 +218,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
"fails_b4_alert": 1,
|
||||
}
|
||||
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
for payload in (agent_payload, policy_payload):
|
||||
# add memory check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -239,7 +261,6 @@ class TestCheckViews(TacticalTestCase):
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}")
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -885,12 +906,12 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make("checks.Check", agent=agent, _quantity=5)
|
||||
unauthorized_check = baker.make(
|
||||
check = baker.make("checks.Check", agent=agent, _quantity=5) # noqa
|
||||
unauthorized_check = baker.make( # noqa
|
||||
"checks.Check", agent=unauthorized_agent, _quantity=7
|
||||
)
|
||||
|
||||
policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)
|
||||
policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) # noqa
|
||||
|
||||
# test super user access
|
||||
self.check_authorized_superuser("get", f"{base_url}/")
|
||||
@@ -973,7 +994,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
for data in [policy_data, agent_data]:
|
||||
for data in (policy_data, agent_data):
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
@@ -1007,8 +1028,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
|
||||
policy_check = baker.make("checks.Check", policy=policy)
|
||||
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
for method in ("get", "put", "delete"):
|
||||
url = f"{base_url}/{check.id}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_check.id}/"
|
||||
policy_url = f"{base_url}/{policy_check.id}/"
|
||||
@@ -1047,7 +1067,6 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_check_action_permissions(self, nats_cmd):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make("checks.Check", agent=agent)
|
||||
@@ -1061,7 +1080,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
assigned_check=unauthorized_check,
|
||||
)
|
||||
|
||||
for action in ["reset", "run"]:
|
||||
for action in ("reset", "run"):
|
||||
if action == "reset":
|
||||
url = f"{base_url}/{check_result.id}/{action}/"
|
||||
unauthorized_url = (
|
||||
|
||||
@@ -6,6 +6,8 @@ urlpatterns = [
|
||||
path("", views.GetAddChecks.as_view()),
|
||||
path("<int:pk>/", views.GetUpdateDeleteCheck.as_view()),
|
||||
path("<int:pk>/reset/", views.ResetCheck.as_view()),
|
||||
path("<agent:agent_id>/resetall/", views.ResetAllChecksStatus.as_view()),
|
||||
path("<agent:agent_id>/run/", views.run_checks),
|
||||
path("<int:pk>/history/", views.GetCheckHistory.as_view()),
|
||||
path("<str:target>/<int:pk>/csbulkrun/", views.bulk_run_checks),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
from datetime import datetime as dt
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
@@ -13,12 +13,14 @@ from rest_framework.views import APIView
|
||||
from agents.models import Agent
|
||||
from alerts.models import Alert
|
||||
from automation.models import Policy
|
||||
from tacticalrmm.constants import CheckStatus, CheckType
|
||||
from tacticalrmm.constants import AGENT_DEFER, CheckStatus, CheckType
|
||||
from tacticalrmm.exceptions import NatsDown
|
||||
from tacticalrmm.helpers import notify_error
|
||||
from tacticalrmm.nats_utils import abulk_nats_command
|
||||
from tacticalrmm.permissions import _has_perm_on_agent
|
||||
|
||||
from .models import Check, CheckHistory, CheckResult
|
||||
from .permissions import ChecksPerms, RunChecksPerms
|
||||
from .permissions import BulkRunChecksPerms, ChecksPerms, RunChecksPerms
|
||||
from .serializers import CheckHistorySerializer, CheckSerializer
|
||||
|
||||
|
||||
@@ -37,7 +39,6 @@ class GetAddChecks(APIView):
|
||||
return Response(CheckSerializer(checks, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
data = request.data.copy()
|
||||
# Determine if adding check to Agent and replace agent_id with pk
|
||||
if "agent" in data.keys():
|
||||
@@ -121,15 +122,54 @@ class ResetCheck(APIView):
|
||||
result.save()
|
||||
|
||||
# resolve any alerts that are open
|
||||
alert = Alert.create_or_return_check_alert(
|
||||
if alert := Alert.create_or_return_check_alert(
|
||||
result.assigned_check, agent=result.agent, skip_create=True
|
||||
)
|
||||
if alert:
|
||||
):
|
||||
alert.resolve()
|
||||
|
||||
return Response("The check status was reset")
|
||||
|
||||
|
||||
class ResetAllChecksStatus(APIView):
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
def post(self, request, agent_id):
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER)
|
||||
.select_related(
|
||||
"policy",
|
||||
"policy__alert_template",
|
||||
"alert_template",
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"checkresults",
|
||||
queryset=CheckResult.objects.select_related("assigned_check"),
|
||||
),
|
||||
"agentchecks",
|
||||
),
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
if not _has_perm_on_agent(request.user, agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
for check in agent.get_checks_with_policies():
|
||||
try:
|
||||
result = check.check_result
|
||||
result.status = CheckStatus.PASSING
|
||||
result.save()
|
||||
if alert := Alert.create_or_return_check_alert(
|
||||
result.assigned_check, agent=agent, skip_create=True
|
||||
):
|
||||
alert.resolve()
|
||||
except:
|
||||
# check hasn't run yet, no check result entry
|
||||
continue
|
||||
|
||||
return Response("All checks status were reset")
|
||||
|
||||
|
||||
class GetCheckHistory(APIView):
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
@@ -169,6 +209,37 @@ def run_checks(request, agent_id):
|
||||
if r == "busy":
|
||||
return notify_error(f"Checks are already running on {agent.hostname}")
|
||||
elif r == "ok":
|
||||
return Response(f"Checks will now be re-run on {agent.hostname}")
|
||||
else:
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response(f"Checks will now be run on {agent.hostname}")
|
||||
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, BulkRunChecksPerms])
|
||||
def bulk_run_checks(request, target, pk):
|
||||
q = Q()
|
||||
match target:
|
||||
case "client":
|
||||
q = Q(site__client__id=pk)
|
||||
case "site":
|
||||
q = Q(site__id=pk)
|
||||
|
||||
agent_ids = list(
|
||||
Agent.objects.only("agent_id", "site")
|
||||
.filter(q)
|
||||
.values_list("agent_id", flat=True)
|
||||
)
|
||||
|
||||
if not agent_ids:
|
||||
return notify_error("No agents matched query")
|
||||
|
||||
payload = {"func": "runchecks"}
|
||||
items = [(agent_id, payload) for agent_id in agent_ids]
|
||||
|
||||
try:
|
||||
asyncio.run(abulk_nats_command(items=items))
|
||||
except NatsDown as e:
|
||||
return notify_error(str(e))
|
||||
|
||||
ret = f"Checks will now be run on {len(agent_ids)} agents"
|
||||
return Response(ret)
|
||||
|
||||
@@ -70,11 +70,11 @@ class Client(BaseAuditModel):
|
||||
sites = self.sites.all()
|
||||
if old_client.workstation_policy != self.workstation_policy:
|
||||
for site in sites:
|
||||
cache.delete_many_pattern(f"site_workstation_{site.pk}_*")
|
||||
cache.delete_many_pattern(f"site_workstation_*{site.pk}_*")
|
||||
|
||||
if old_client.server_policy != self.server_policy:
|
||||
for site in sites:
|
||||
cache.delete_many_pattern(f"site_server_{site.pk}_*")
|
||||
cache.delete_many_pattern(f"site_server_*{site.pk}_*")
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -145,10 +145,10 @@ class Site(BaseAuditModel):
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
if old_site.workstation_policy != self.workstation_policy:
|
||||
cache.delete_many_pattern(f"site_workstation_{self.pk}_*")
|
||||
cache.delete_many_pattern(f"site_workstation_*{self.pk}_*")
|
||||
|
||||
if old_site.server_policy != self.server_policy:
|
||||
cache.delete_many_pattern(f"site_server_{self.pk}_*")
|
||||
cache.delete_many_pattern(f"site_server_*{self.pk}_*")
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -229,16 +229,16 @@ class ClientCustomField(models.Model):
|
||||
return self.multiple_value
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return self.string_value
|
||||
|
||||
return self.string_value
|
||||
|
||||
def save_to_field(self, value):
|
||||
if self.field.type in [
|
||||
if self.field.type in (
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
):
|
||||
self.string_value = value
|
||||
self.save()
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
@@ -280,16 +280,16 @@ class SiteCustomField(models.Model):
|
||||
return self.multiple_value
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return self.string_value
|
||||
|
||||
return self.string_value
|
||||
|
||||
def save_to_field(self, value):
|
||||
if self.field.type in [
|
||||
if self.field.type in (
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
):
|
||||
self.string_value = value
|
||||
self.save()
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
|
||||
@@ -12,7 +12,7 @@ class ClientsPerms(permissions.BasePermission):
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_clients")
|
||||
elif r.method == "PUT" or r.method == "DELETE":
|
||||
elif r.method in ("PUT", "DELETE"):
|
||||
return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
@@ -29,7 +29,7 @@ class SitesPerms(permissions.BasePermission):
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_sites")
|
||||
elif r.method == "PUT" or r.method == "DELETE":
|
||||
elif r.method in ("PUT", "DELETE"):
|
||||
return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
@@ -41,5 +41,5 @@ class DeploymentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_deployments")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user