Compare commits

..

200 Commits

Author SHA1 Message Date
wh1te909
498748217d Release 0.15.5 2022-12-08 07:08:24 +00:00
wh1te909
483bf331fa bump version 2022-12-08 06:59:40 +00:00
wh1te909
9d62b4acdd use iterator instead of all in case large queryset 2022-12-08 06:59:02 +00:00
wh1te909
c9deef6e76 add a migration for old tasks 2022-12-08 05:34:41 +00:00
wh1te909
8ba6f8b0e1 fix keyerror for old tasks 2022-12-07 20:26:24 +00:00
wh1te909
824cbdc84b Release 0.15.4 2022-12-04 23:42:32 +00:00
wh1te909
448c59ea88 bump docker nats 2022-12-04 23:33:05 +00:00
wh1te909
91b858bf33 bump versions 2022-12-04 23:31:57 +00:00
wh1te909
c12bede980 update bin 2022-12-04 07:14:05 +00:00
wh1te909
71e9fa3d16 update name and deps 2022-12-04 07:11:01 +00:00
wh1te909
6800b9aaae fix mgmt command 2022-12-04 06:14:37 +00:00
wh1te909
77d44f25f9 create and log an agent history event for alert failure/resolved script runs 2022-12-04 02:11:49 +00:00
wh1te909
ab6227828b add env vars for script checks 2022-12-03 09:56:44 +00:00
wh1te909
719ba56c59 remove db hit 2022-12-03 09:55:37 +00:00
wh1te909
dacedf4018 add ram check 2022-12-03 08:14:53 +00:00
wh1te909
2526fa3c47 fix backup/restore when OS are different 2022-12-01 06:33:27 +00:00
wh1te909
7e2295c382 rework celery config 2022-12-01 00:22:09 +00:00
wh1te909
6ef02004ff update reqs 2022-11-30 23:56:18 +00:00
wh1te909
0e60d062e9 feat: env vars 2022-11-30 23:25:52 +00:00
wh1te909
80a94f97c4 add ws compression option 2022-11-30 23:25:52 +00:00
wh1te909
c18bc5fe67 switch to stringio 2022-11-30 23:25:52 +00:00
wh1te909
02b98a2429 don't call now twice 2022-11-30 23:25:52 +00:00
Dan
0383aeaa87 Merge pull request #1358 from styx-tdo/develop
Fix wrong service name
2022-11-30 12:49:25 -08:00
Spam Me
15a41d532e Fix wrong service name 2022-11-30 15:01:17 +01:00
wh1te909
0f49725789 update reqs 2022-11-22 19:13:44 +00:00
wh1te909
1db6733e66 add checkin config 2022-11-22 19:06:18 +00:00
wh1te909
0343ee4f6b fix mypy 2022-11-13 08:05:42 +00:00
wh1te909
2c37d2233a add flake8 2022-11-13 07:44:23 +00:00
wh1te909
0cb8ccfddd back to dev [skip ci] 2022-11-13 01:53:05 +00:00
wh1te909
41c0e85d00 Release 0.15.3 2022-11-13 01:50:43 +00:00
wh1te909
35b1a39ed8 bump versions 2022-11-13 01:36:39 +00:00
wh1te909
61a577ba70 add arch check 2022-11-12 20:58:29 +00:00
wh1te909
a1e32584fa bump docker nats 2022-11-12 20:57:57 +00:00
wh1te909
28e0ee536d update reqs 2022-11-12 20:28:27 +00:00
wh1te909
9d64a9c038 update wheel 2022-11-08 21:01:16 +00:00
wh1te909
702ba969c2 revert caching of the check results. will refactor it and do another way 2022-11-08 21:00:49 +00:00
wh1te909
6dde8ee2b8 update reqs 2022-11-08 07:23:22 +00:00
wh1te909
018420310c code cleanup 2022-11-08 07:22:31 +00:00
wh1te909
6d49d34033 update go deps 2022-11-08 07:21:25 +00:00
wh1te909
1fbd403164 fix params 2022-11-08 07:16:32 +00:00
wh1te909
13f544d2be add more caching to speed up agent table loading 2022-11-03 07:35:01 +00:00
wh1te909
3c9e64de81 update reqs 2022-11-03 07:32:04 +00:00
Dan
5a9bafbc32 Merge pull request #1294 from Can-eh-dian11/develop
ability to bulk delete using client and site
2022-10-29 14:27:24 -07:00
wh1te909
b89d96b66f add python 3.11 to testing matrix 2022-10-28 19:16:17 +00:00
wh1te909
b7176191ac update reqs 2022-10-28 04:55:31 +00:00
wh1te909
453c5f47c2 just kidding 2022-10-28 04:43:28 +00:00
wh1te909
eea62e1263 fixed slow dashboard loading due to bad query 2022-10-28 01:02:53 +00:00
Scott MacDowall
4fb2a0f1ca Merge branch 'amidaware:develop' into develop 2022-10-25 23:02:02 -04:00
wh1te909
1d102ef096 Release 0.15.2 2022-10-25 22:13:29 +00:00
wh1te909
bf3c65778e bump versions [skip ci] 2022-10-25 22:10:50 +00:00
wh1te909
df7fe3e6b4 bump mesh [skip ci] 2022-10-25 21:40:44 +00:00
wh1te909
b657468b62 update uninstall for new path 2022-10-25 19:03:57 +00:00
wh1te909
4edc0058d3 update reqs 2022-10-25 06:32:51 +00:00
wh1te909
2c3b35293b bump versions 2022-10-25 06:32:38 +00:00
wh1te909
be0c9a4d46 add path 2022-10-25 05:59:14 +00:00
wh1te909
dd4140558e fix tests 2022-10-21 16:51:52 +00:00
wh1te909
71c2519b8e fix wording 2022-10-21 16:40:11 +00:00
wh1te909
badfc26aed fix fake agents script 2022-10-21 16:39:55 +00:00
wh1te909
b2bc3adb3d update demo 2022-10-20 19:03:37 +00:00
wh1te909
5ccf408fd6 back to dev [skip ci] 2022-10-19 23:14:45 +00:00
wh1te909
da185875bb Release 0.15.1 2022-10-19 22:51:33 +00:00
wh1te909
af16912541 bump versions [skip ci] 2022-10-19 22:50:59 +00:00
wh1te909
1bf9e2a5e6 update reqs 2022-10-19 06:50:01 +00:00
wh1te909
5a572651ff add token expired check 2022-10-19 06:45:53 +00:00
wh1te909
5a191e387f update tests 2022-10-19 04:02:21 +00:00
wh1te909
18f29f5790 update reqs 2022-10-18 19:54:48 +00:00
wh1te909
054a73e0f8 fix tests 2022-10-18 05:07:00 +00:00
wh1te909
14824db7b0 fix tests 2022-10-18 05:02:35 +00:00
wh1te909
721c48ea88 python 3.10.8 2022-10-18 04:54:04 +00:00
wh1te909
ed7bfcfb58 daphne/channels 4 2022-10-18 04:52:04 +00:00
wh1te909
773a40a126 bump docker nats 2022-10-18 04:50:48 +00:00
wh1te909
961252ef26 rework uwsgi conf 2022-10-18 00:23:10 +00:00
wh1te909
a2650f3c47 update setuptools 2022-10-18 00:21:11 +00:00
wh1te909
d71ee194e1 add optional nats monitoring 2022-10-18 00:12:57 +00:00
wh1te909
22e1a4cf41 update reqs 2022-10-14 07:41:16 +00:00
wh1te909
a50bf901d3 add check for wget fixes #1317 2022-10-14 07:39:27 +00:00
wh1te909
c9469635b5 remove unneeded chmod fixes #1307 2022-10-14 07:21:43 +00:00
Dan
36df3278e5 Merge pull request #1311 from dinger1986/develop
Update agent_linux.sh
2022-10-11 16:14:06 -07:00
dinger1986
cb2258aaa8 Update agent_linux.sh 2022-10-10 23:53:24 +01:00
wh1te909
0391d9eb7e update reqs 2022-10-10 17:07:00 +00:00
Dan
12698b4c20 Merge pull request #1310 from dinger1986/develop
Update agent_linux.sh
2022-10-10 10:01:50 -07:00
dinger1986
f7b9d459ab Update agent_linux.sh 2022-10-10 17:51:35 +01:00
wh1te909
65ab14e68b cleanup socket fixes #1210 2022-10-07 17:15:37 +00:00
Dan
93a5dd5de4 Merge pull request #1213 from stavros-k/json
Updating meshcentral's config casing to match upstreams json schema.
2022-10-06 09:38:16 -07:00
Dan
61807bdaaa Merge pull request #1301 from af7567/policyselect2
break from loop once a valid policy is found. remove old unused list
2022-10-06 09:36:59 -07:00
Dan
a1a5d1adba Merge pull request #1295 from silversword411/develop
typo
2022-10-06 09:36:20 -07:00
silversword411
9dd4aefea5 Merge branch 'amidaware:develop' into develop 2022-10-06 07:52:47 -04:00
Adam
db4540089a remove parentheses from if statement to fix codestyle test
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 13:17:48 +01:00
Adam
24c899c91a break from loop once a valid policy is found. remove old unused list
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 12:05:32 +01:00
wh1te909
ade1a73966 code cleanup and optimizations 2022-10-02 21:42:46 +00:00
wh1te909
fb9ec2b040 update reqs 2022-10-02 21:42:03 +00:00
wh1te909
3a683812e9 change tempdir 2022-10-02 21:37:04 +00:00
wh1te909
6d317603c9 fix script snippets fixes #1298 2022-09-27 23:21:26 +00:00
silversword411
5a3d2d196c typo 2022-09-25 17:21:17 -04:00
Scott MacDowall
e740c4d980 fix formatting 2022-09-25 14:59:05 -04:00
Scott MacDowall
253e4596e2 ability to bulk delete using client and site 2022-09-25 14:32:40 -04:00
wh1te909
70e75a355c Release 0.15.0 2022-09-24 03:09:12 +00:00
wh1te909
4f885c9a79 bump versions 2022-09-24 03:08:44 +00:00
wh1te909
b519d2afac update readme for macOS support 2022-09-24 03:07:38 +00:00
wh1te909
6b61e3b76b bump docker nats 2022-09-24 02:52:12 +00:00
wh1te909
30b9c72c31 bump nats 2022-09-24 01:51:59 +00:00
wh1te909
385bf74f6e change dl cmd 2022-09-24 01:51:52 +00:00
wh1te909
be5615e530 bump web dev 2022-09-23 23:07:48 +00:00
wh1te909
d81a03c093 mac agent 2022-09-23 22:57:29 +00:00
wh1te909
f8249c8267 switch to corp funding 2022-09-23 22:10:05 +00:00
wh1te909
5a1cbdcd3b update reqs 2022-09-23 06:59:44 +00:00
wh1te909
e0c99d87bd update template 2022-09-23 06:59:26 +00:00
wh1te909
548250029d add missing field to serializer fixes #1283 2022-09-13 19:18:16 +00:00
wh1te909
66a354dbdc back to dev [skip ci] 2022-09-12 07:03:05 +00:00
wh1te909
834e602686 Release 0.14.8 2022-09-12 06:57:39 +00:00
wh1te909
1f693ca4f6 bump versions 2022-09-12 06:50:55 +00:00
wh1te909
97a0bc6045 optimize query to use less ram 2022-09-12 05:33:07 +00:00
wh1te909
8b75cdfefd update bin 2022-09-12 01:44:21 +00:00
wh1te909
917aecf1ff update deps and go 1.19 2022-09-12 01:42:24 +00:00
wh1te909
663dcd0396 fix tests 2022-09-11 20:51:57 +00:00
wh1te909
8f2dffb1ad update reqs 2022-09-11 20:29:45 +00:00
wh1te909
20228e3d19 add postgres ready check 2022-09-11 20:23:18 +00:00
wh1te909
81c6cc11b3 update supported version 2022-09-11 01:37:45 +00:00
wh1te909
2ccacbe5f3 fix for newer mesh 2022-09-11 01:33:00 +00:00
Dan
a5345e8468 Merge pull request #1277 from af7567/winupdatesfix
When checking if patches are to be installed, don't exit the function after finding a patched machine.
2022-09-06 12:02:26 -07:00
Dan
8f5d62bb81 Merge pull request #1273 from silversword411/develop
Adding client and site to mgmt command find_software
2022-09-06 11:59:07 -07:00
Adam
28f6838560 continue to next agent rather than return from function, otherwise other agents won't receive updates
Signed-off-by: Adam <adam@csparker.co.uk>
2022-09-04 18:09:58 +01:00
silversword411
c86aacb31c format tweak 2022-09-02 17:24:33 -04:00
silversword411
f62f5192d6 Adding client and site to mgmt command find_software 2022-09-02 14:28:45 -04:00
wh1te909
b14ea1fe3e fix maintenance mode and assigning policy via agent context menu not saving 2022-08-24 08:29:13 +00:00
wh1te909
552633a00b fix configs 2022-08-24 08:27:50 +00:00
wh1te909
7faba2a690 add migrations for updated pytz 2022-08-24 07:33:55 +00:00
wh1te909
db910aff06 back to dev 2022-08-23 06:02:48 +00:00
wh1te909
72126052ad Release 0.14.7 2022-08-23 05:59:29 +00:00
wh1te909
75d9f6a7e7 bump versions 2022-08-23 05:09:55 +00:00
wh1te909
de677294c6 refactor 2022-08-23 00:54:07 +00:00
wh1te909
da1e6b8259 update reqs 2022-08-22 06:47:52 +00:00
wh1te909
a9633b3990 update reqs 2022-08-20 20:09:53 +00:00
sadnub
7ac9af1cc1 fix sed command to do an inplace update of nginx.conf 2022-08-17 11:52:54 -04:00
Dan
00d8b8cd61 Merge pull request #1244 from dinger1986/develop
Update troubleshoot_server.sh
2022-08-17 08:43:13 -07:00
dinger1986
af7ff7f5cf Update troubleshoot_server.sh
Change SSL check as wasnt working and add output
2022-08-17 11:42:05 +01:00
wh1te909
2a20719130 fix demo script check history graph 2022-08-17 06:57:05 +00:00
sadnub
f481940180 fix tests 2022-08-14 21:56:09 -04:00
sadnub
ca8824d1e3 fix clients not filtering by role in policy overview 2022-08-14 21:49:38 -04:00
sadnub
f4be199b77 fix checks/tasks return cache values for other plats 2022-08-14 19:45:11 -04:00
sadnub
6bcef8334e fix nginx entypoint 2022-08-14 10:26:55 -04:00
wh1te909
3955eff683 more dev setup 2022-08-14 08:17:08 +00:00
wh1te909
aa0f6ecd75 python 3.10.6 2022-08-14 08:14:59 +00:00
wh1te909
ef4a94ed78 more ansible 2022-08-12 17:21:25 +00:00
wh1te909
b5c803ce65 update reqs 2022-08-12 17:19:04 +00:00
wh1te909
d4325ed82e more ansible dev 2022-08-12 06:15:21 +00:00
wh1te909
3805fb8f26 expose more fields in search amidaware/tacticalrmm-web@93dbc74e33 closes #652 2022-08-12 01:23:33 +00:00
wh1te909
d4d938c655 update paths 2022-08-12 01:06:56 +00:00
wh1te909
1c6911e361 start moving to nested serializers 2022-08-10 07:15:23 +00:00
wh1te909
a1b364f337 drop support for agent < 2.0.0 2022-08-10 07:12:51 +00:00
wh1te909
ece5c3da86 update reqs 2022-08-10 00:49:20 +00:00
wh1te909
5d1ae6047b back to dev 2022-08-10 00:38:13 +00:00
wh1te909
5605c72253 Release 0.14.6 2022-08-09 21:47:20 +00:00
wh1te909
66bbcf0733 fix tests 2022-08-09 21:35:23 +00:00
wh1te909
acc23ea7bb bump versions 2022-08-09 21:18:41 +00:00
wh1te909
663bd0c9f0 remove dead code 2022-08-09 21:16:18 +00:00
wh1te909
39b1025dfa fix tests 2022-08-05 17:35:40 +00:00
sadnub
d2875e90b2 fix docker dev 2022-08-05 12:10:52 -04:00
wh1te909
ff461d1d02 fixes #1174 amidaware/tacticalrmm-web@76f330fb9c 2022-08-05 07:22:46 +00:00
wh1te909
58164ea2d3 dev 2022-08-05 05:57:38 +00:00
wh1te909
1bf4834004 Django 4.1 2022-08-04 23:43:57 +00:00
wh1te909
bf58d78281 fix return tuple formatting 2022-08-04 23:40:35 +00:00
wh1te909
0dc749bb3d Release 0.14.5 2022-08-01 22:57:01 +00:00
wh1te909
a8aedfde55 bump version 2022-08-01 22:56:21 +00:00
wh1te909
b174a89032 Release 0.14.4 2022-08-01 18:09:18 +00:00
wh1te909
9b92d1b673 bump version 2022-08-01 17:50:33 +00:00
wh1te909
febc9aed11 feat: run as user amidaware/tacticalrmm-web@137a5648ce amidaware/rmmagent@50cebb950d 2022-07-31 22:23:19 +00:00
wh1te909
de2462677e fix working dir 2022-07-31 21:31:58 +00:00
wh1te909
8bd94d46eb fix empty statement 2022-07-28 17:28:49 +00:00
wh1te909
d43cefe28f add file associations for yaml [skip ci] 2022-07-27 07:31:54 +00:00
wh1te909
b82874e261 back to develop 2022-07-27 07:30:53 +00:00
wh1te909
8554cb5d6c Release 0.14.3 2022-07-27 07:18:55 +00:00
wh1te909
f901614056 bump version 2022-07-27 06:11:41 +00:00
wh1te909
b555d217ab remove check 2022-07-27 06:10:33 +00:00
wh1te909
775c600234 docker nginx changes 2022-07-27 04:19:17 +00:00
wh1te909
128f2570b8 catch exception if mesh is down 2022-07-27 02:01:29 +00:00
wh1te909
3cd53e79b4 add signing key 2022-07-27 01:59:55 +00:00
wh1te909
ebba84ffda switch to official nginx repo to get latest version 2022-07-26 08:09:49 +00:00
wh1te909
1e1a42fe98 update web ver 2022-07-26 08:08:43 +00:00
wh1te909
8a744a440d update reqs 2022-07-26 07:47:21 +00:00
wh1te909
f4fc3c7d55 unused var 2022-07-26 04:39:11 +00:00
wh1te909
0594d121de add agent ver to status closes #1224 2022-07-24 01:18:54 +00:00
wh1te909
12c85d6234 start ansible role to deploy dev environment 2022-07-20 07:16:47 +00:00
Stavros kois
87d05223af apply json fix to docker aswell 2022-07-18 22:08:06 +03:00
Stavros kois
babf6366e8 chore(install script): use same casing as the json schema 2022-07-18 21:13:20 +03:00
wh1te909
5e37728f66 remove devskum 2022-07-18 17:22:24 +00:00
wh1te909
e8e19fede7 don't allow dates in past #1174 2022-07-18 08:04:15 +00:00
wh1te909
e565dbfa66 invalidate cache on policy script change 2022-07-12 20:16:07 +00:00
wh1te909
d180d6820c back to develop 2022-07-10 03:35:15 +00:00
wh1te909
7f252e9b7c Release 0.14.2 2022-07-10 03:34:02 +00:00
wh1te909
41db8681f8 no sudo 2022-07-10 00:38:09 +00:00
wh1te909
26cd58fd6d bump version 2022-07-10 00:16:32 +00:00
wh1te909
63c7e1aa9d update reqs 2022-07-10 00:16:13 +00:00
wh1te909
d5a6063e5e remove extra space 2022-07-09 09:31:05 +00:00
wh1te909
00affdbdec update supported version 2022-07-09 09:30:58 +00:00
wh1te909
db3f0bbd4f increase nginx open file limit 2022-07-09 08:09:24 +00:00
wh1te909
020a59cb97 remove un-needed expose 2022-07-09 08:08:09 +00:00
wh1te909
ff4fa6402d back to dev 2022-07-08 06:40:55 +00:00
wh1te909
80f7555499 Release 0.14.1 2022-07-08 06:40:15 +00:00
wh1te909
10cc187c5d bump versions 2022-07-08 06:38:20 +00:00
144 changed files with 3968 additions and 1017 deletions

View File

@@ -1,11 +1,11 @@
# pulls community scripts from git repo
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
FROM python:3.10.8-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.10.8-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -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

View File

@@ -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}"
@@ -142,16 +139,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}"

View File

@@ -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
View File

@@ -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

View File

@@ -14,22 +14,23 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ['3.10.4']
python-version: ["3.10.8", "3.11.0"]
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: "14"
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"

View File

@@ -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

1
.gitignore vendored
View File

@@ -55,3 +55,4 @@ coverage.lcov
daphne.sock.lock
.pytest_cache
coverage.xml
setup_dev.yml

17
.vscode/settings.json vendored
View File

@@ -1,7 +1,10 @@
{
"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.extraPaths": [
"api/tacticalrmm",
"api/env"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
@@ -22,11 +25,17 @@
"**env/**"
],
"python.formatting.provider": "black",
"mypy.targets": ["api/tacticalrmm"],
"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,
@@ -66,4 +75,4 @@
"completeUnimported": true,
"staticcheck": true
}
}
}

View File

@@ -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)

View File

@@ -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
View File

@@ -0,0 +1,3 @@
### tacticalrmm ansible WIP
ansible role to setup a Debian 11 VM for tacticalrmm local development

View File

@@ -0,0 +1,40 @@
---
user: "tactical"
python_ver: "3.10.8"
go_ver: "1.18.5"
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

View 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/*;
}

View File

@@ -0,0 +1,2 @@
deb https://nginx.org/packages/debian/ bullseye nginx
deb-src https://nginx.org/packages/debian/ bullseye nginx

View 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

View File

@@ -0,0 +1,633 @@
---
- 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: download and install golang
tags: golang
become: yes
ansible.builtin.unarchive:
src: "https://go.dev/dl/go{{ go_ver }}.linux-amd64.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 bullseye-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-14
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 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 }}"
- 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-amd64.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-amd64/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_16.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.copy:
src: nginx.repo
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: copy nats-api bin
tags: nats-api
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ backend_dir }}/natsapi/bin/nats-api"
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.10 -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: import mongodb repo signing key
tags: mongo
become: yes
ansible.builtin.apt_key:
url: https://www.mongodb.org/static/pgp/server-4.4.asc
state: present
- name: setup mongodb repo
tags: mongo
become: yes
ansible.builtin.copy:
content: "deb https://repo.mongodb.org/apt/debian buster/mongodb-org/4.4 main"
dest: /etc/apt/sources.list.d/mongodb-org-4.4.list
owner: root
group: root
mode: "0644"
- name: install mongodb
tags: mongo
become: yes
ansible.builtin.apt:
pkg: mongodb-org
state: present
update_cache: yes
- name: ensure mongodb enabled and started
tags: mongo
become: yes
ansible.builtin.service:
name: mongod
enabled: yes
state: started
- 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

View 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;
}
}

View File

@@ -0,0 +1,25 @@
SECRET_KEY = "{{ django_secret }}"
DEBUG = True
ALLOWED_HOSTS = ['{{ api }}']
ADMIN_URL = "admin/"
CORS_ORIGIN_WHITELIST = [
"http://{{ rmm }}:8080",
"https://{{ rmm }}:8080",
]
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_src }}"
KEY_FILE = "{{ privkey_src }}"
MESH_USERNAME = "{{ mesh_user }}"
MESH_SITE = "https://{{ mesh }}"

View File

@@ -0,0 +1,33 @@
{
"settings": {
"Cert": "{{ mesh }}",
"MongoDb": "mongodb://127.0.0.1:27017",
"MongoDbName": "meshcentral",
"WANonly": true,
"Minify": 1,
"Port": 4430,
"AliasPort": 443,
"RedirPort": 800,
"AllowLoginToken": true,
"AllowFraming": true,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"TlsOffload": "127.0.0.1",
"agentCoreDump": false,
"Compression": true,
"WsCompression": true,
"AgentWsCompression": true,
"MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 }
},
"domains": {
"": {
"Title": "Tactical RMM",
"Title2": "Tactical RMM",
"NewAccounts": false,
"CertUrl": "https://{{ mesh }}:443/",
"GeoLocation": true,
"CookieIpCheck": false,
"mstsc": true
}
}
}

View 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;
}
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=MeshCentral Server
After=network.target mongod.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

View 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

View 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

View File

@@ -0,0 +1,4 @@
DEV_URL = "http://{{ api }}:8000"
DEV_HOST = "{{ rmm }}"
DEV_PORT = "8080"
USE_HTTPS = false

View File

@@ -0,0 +1,20 @@
---
- 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"
django_secret: "changeme"
django_user: "changeme"
django_password: "changeme"
roles:
- trmm_dev

12
api/tacticalrmm/.flake8 Normal file
View 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/*

View File

@@ -1,5 +1,6 @@
import os
import subprocess
from contextlib import suppress
import pyotp
from django.core.management.base import BaseCommand
@@ -25,7 +26,7 @@ class Command(BaseCommand):
nginx = "/etc/nginx/sites-available/frontend.conf"
found = None
if os.path.exists(nginx):
try:
with suppress(Exception):
with open(nginx, "r") as f:
for line in f:
if "server_name" in line:
@@ -35,8 +36,6 @@ class Command(BaseCommand):
if found:
rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32()
user.totp_key = code

View File

@@ -31,8 +31,8 @@ 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):

View File

@@ -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)

View 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

View File

@@ -22,18 +22,7 @@ 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):
@@ -93,7 +82,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 +148,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 +159,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 +169,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 +184,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 = ""
@@ -278,7 +268,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")

View File

@@ -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,16 @@ 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(
"--delete",
action="store_true",
@@ -32,11 +42,15 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
days = kwargs["days"]
agentver = kwargs["agentver"]
site = kwargs["site"]
client = kwargs["client"]
delete = kwargs["delete"]
if not days and not agentver:
if not days and not agentver and not site and not client:
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, or client"
)
)
return
@@ -50,6 +64,12 @@ class Command(BaseCommand):
if agentver:
agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)]
if site:
agents = [i for i in q if i.site.name == site]
if client:
agents = [i for i in q if i.client.name == client]
if not agents:
self.stdout.write(self.style.ERROR("No agents matched"))
return
@@ -64,7 +84,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

View File

@@ -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")
@@ -177,6 +180,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 +199,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")
@@ -313,18 +319,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 +347,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 +581,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 +614,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 +737,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:

View 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,
),
),
]

View File

@@ -1,6 +1,7 @@
import asyncio
import re
from collections import Counter
from contextlib import suppress
from distutils.version import LooseVersion
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
@@ -17,6 +18,7 @@ from nats.errors import TimeoutError
from packaging import version as pyver
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,
@@ -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"
@@ -479,7 +486,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",
@@ -532,12 +539,18 @@ 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)
data = {
@@ -548,6 +561,8 @@ class Agent(BaseAuditModel):
"code": script.code,
"shell": script.shell,
},
"run_as_user": run_as_user,
"env_vars": env_vars,
}
if history_pk != 0:
@@ -583,7 +598,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 +630,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 +689,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 +754,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 +779,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 +790,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:
@@ -791,6 +803,7 @@ class Agent(BaseAuditModel):
options = {
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
"user": "tacticalrmm",
"name": "trmm-django",
"password": settings.SECRET_KEY,
"connect_timeout": 3,
"max_reconnect_attempts": 2,
@@ -829,9 +842,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 +855,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 +880,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)
@@ -889,8 +905,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
@@ -1009,16 +1023,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:

View File

@@ -122,5 +122,5 @@ 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")

View File

@@ -90,26 +90,31 @@ 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()
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 +146,18 @@ class AgentTableSerializer(serializers.ModelSerializer):
"plat",
"goarch",
"has_patches_pending",
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
]
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")

View File

@@ -153,6 +153,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 +165,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(

View File

@@ -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]

View File

@@ -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)

View File

@@ -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,6 +568,8 @@ 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)
@@ -564,6 +579,8 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18,
emails=[],
args=["abc", "123"],
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
email_task.reset_mock()
@@ -577,6 +594,8 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18,
emails=["admin@example.com", "bob@example.com"],
args=["abc", "123"],
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
# test fire and forget
@@ -585,6 +604,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 +615,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 +635,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 +651,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 +670,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 +686,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 +707,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 +723,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 +741,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 +757,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()
@@ -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
)

View File

@@ -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
@@ -51,12 +52,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 +70,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"
)

View File

@@ -1,17 +1,19 @@
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 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
@@ -31,15 +33,16 @@ 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,
@@ -113,7 +116,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",
@@ -132,18 +135,12 @@ class GetAgents(APIView):
queryset=CheckResult.objects.select_related("assigned_check"),
),
)
.annotate(
pending_actions_count=Count(
"pendingactions",
filter=Q(pendingactions__status=PAStatus.PENDING),
)
)
.annotate(
has_patches_pending=Exists(
WinUpdate.objects.filter(
agent_id=OuterRef("pk"), action="approve", installed=False
)
)
),
)
)
serializer = AgentTableSerializer(agents, many=True)
@@ -165,6 +162,25 @@ 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)
@@ -174,9 +190,9 @@ class GetUpdateDeleteAgent(APIView):
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
@@ -215,18 +231,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 +265,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 +276,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 +408,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 +431,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 +460,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 +477,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)
)
@@ -519,7 +547,15 @@ 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(
"Missing code signing token, or token is no longer valid. Please read the docs 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()
@@ -528,6 +564,21 @@ def install_agent(request):
user=installer_user, expiry=dt.timedelta(hours=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,14 +596,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
@@ -566,52 +609,45 @@ 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,
}
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)
)
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 +665,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 +699,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 +725,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 +740,8 @@ def run_script(request, agent_id):
nats_timeout=req_timeout,
emails=emails,
args=args,
run_as_user=run_as_user,
env_vars=env_vars,
)
elif output == "collector":
from core.models import CustomField
@@ -728,6 +752,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 +782,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,6 +905,8 @@ 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]
@@ -897,7 +932,7 @@ def bulk(request):
shell,
request.data["timeout"],
request.user.username[:50],
run_on_offline=request.data["offlineAgents"],
request.data["run_as_user"],
)
return Response(f"Command will now be run on {len(agents)} agents")
@@ -909,6 +944,8 @@ def bulk(request):
request.data["args"],
request.data["timeout"],
request.user.username[:50],
request.data["run_as_user"],
request.data["env_vars"],
)
return Response(f"{script.name} will now be run on {len(agents)} agents")
@@ -956,10 +993,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"])

View File

@@ -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,
),
),
]

View File

@@ -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,
@@ -268,7 +269,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
@@ -462,13 +463,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 +500,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 +594,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
@@ -615,7 +634,7 @@ class Alert(models.Model):
if not args:
return []
temp_args = list()
temp_args = []
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
@@ -659,6 +678,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 +698,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

View File

@@ -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")

View File

@@ -677,8 +677,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()
@@ -1375,6 +1373,7 @@ class TestAlertTasks(TacticalTestCase):
):
from agents.tasks import agent_outages_task
from agents.models import AgentHistory
# Setup cmd mock
success = {
@@ -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)
@@ -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)

View File

@@ -41,13 +41,13 @@ class GetAddAlerts(APIView):
elif any(
key
in [
in (
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
)
for key in request.data.keys()
):
clientFilter = Q()

View File

@@ -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)

View File

@@ -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()),
]

View 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),
)

View File

@@ -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
@@ -398,26 +400,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)
@@ -508,7 +517,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 +570,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())

View File

@@ -218,12 +218,12 @@ class Policy(BaseAuditModel):
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():
if policy and policy.active and policy.pk not in processed_policies:
@@ -244,10 +244,10 @@ 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():
if policy and policy.active and policy.pk not in processed_policies:
@@ -263,24 +263,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:

View File

@@ -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")

View File

@@ -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):

View File

@@ -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)

View File

@@ -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
@@ -108,8 +109,18 @@ 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)
@@ -135,6 +146,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"])

View File

@@ -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),
]

View File

@@ -1,6 +1,7 @@
import asyncio
import random
import string
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import pytz
@@ -225,15 +226,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()
@@ -262,13 +261,13 @@ 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
@@ -432,10 +431,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,

View File

@@ -33,40 +33,40 @@ class TaskSerializer(serializers.ModelSerializer):
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
@@ -100,13 +100,13 @@ class TaskSerializer(serializers.ModelSerializer):
# 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(
@@ -188,13 +188,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 +228,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 +246,8 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
),
"shell": script.shell,
"timeout": action["timeout"],
"run_as_user": script.run_as_user,
"env_vars": env_vars,
}
)
if actions_to_remove:

View File

@@ -1,6 +1,7 @@
import asyncio
import datetime as dt
import random
from contextlib import suppress
from time import sleep
from typing import Optional, Union
@@ -16,60 +17,64 @@ from tacticalrmm.constants import DebugLogType
@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"

View File

@@ -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")
@@ -258,7 +258,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")
@@ -766,12 +768,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}/")

View 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,
),
),
]

View File

@@ -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,8 +155,8 @@ 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):
@@ -198,10 +204,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,8 +218,8 @@ 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]:
@@ -224,8 +227,6 @@ class Check(BaseAuditModel):
def create_policy_check(self, policy: "Policy") -> None:
fields_to_copy = POLICY_CHECK_FIELDS_TO_COPY
check = Check.objects.create(
policy=policy,
)
@@ -233,7 +234,7 @@ 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()
@@ -335,12 +336,12 @@ class CheckResult(models.Model):
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(

View File

@@ -5,7 +5,7 @@ from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
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"]

View File

@@ -43,13 +43,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 +82,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 +91,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 +113,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 +122,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 +133,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 +142,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 +158,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 +169,13 @@ 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 []
# check's env_vars override the script's env vars
return obj.env_vars or obj.script.env_vars
class Meta:
model = Check
exclude = [

View File

@@ -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)
@@ -239,7 +239,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 +884,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}/")

View File

@@ -169,6 +169,6 @@ 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")

View File

@@ -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:

View File

@@ -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")

View File

@@ -23,7 +23,7 @@ class TestClientViews(TacticalTestCase):
def test_get_clients(self):
# setup data
baker.make("clients.Client", _quantity=5)
clients = Client.objects.all()
clients = Client.objects.all() # noqa
url = f"{base_url}/"
r = self.client.get(url, format="json")
@@ -710,8 +710,8 @@ class TestClientPermissions(TacticalTestCase):
site = baker.make("clients.Site")
other_site = baker.make("clients.Site")
deployments = baker.make("clients.Deployment", site=site, _quantity=5)
other_deployments = baker.make(
deployments = baker.make("clients.Deployment", site=site, _quantity=5) # noqa
other_deployments = baker.make( # noqa
"clients.Deployment", site=other_site, _quantity=7
)

View File

@@ -1,6 +1,7 @@
import datetime as dt
import re
import uuid
from contextlib import suppress
from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects
from django.shortcuts import get_object_or_404
@@ -338,10 +339,8 @@ class AgentDeployment(APIView):
if not _has_perm_on_site(request.user, d.site.pk):
raise PermissionDenied()
try:
with suppress(Exception):
d.auth_token.delete()
except:
pass
d.delete()
return Response("The deployment was deleted")

View File

@@ -67,8 +67,11 @@ RemoveOldAgent() {
InstallMesh() {
if [ -f /etc/os-release ]; then
distroID=$(. /etc/os-release; echo $ID)
distroIDLIKE=$(. /etc/os-release; echo $ID_LIKE)
if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then
set_locale_deb
elif [[ " ${deb[*]} " =~ " ${distroIDLIKE} " ]]; then
set_locale_deb
elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then
set_locale_rhel
else
@@ -78,21 +81,21 @@ InstallMesh() {
meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX")
if [ $? -ne 0 ]; then
meshTmpDir='meshtemp'
meshTmpDir='/root/meshtemp'
mkdir -p ${meshTmpDir}
fi
meshTmpBin="${meshTmpDir}/meshagent"
wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL}
chmod +x ${meshTmpBin}
mkdir -p ${meshDir}
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US ${meshTmpBin} -install --installPath=${meshDir}
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US XAUTHORITY=foo DISPLAY=bar ${meshTmpBin} -install --installPath=${meshDir}
sleep 1
rm -rf ${meshTmpDir}
}
RemoveMesh() {
if [ -f "${meshSystemBin}" ]; then
${meshSystemBin} -uninstall
env XAUTHORITY=foo DISPLAY=bar ${meshSystemBin} -uninstall
sleep 1
fi
@@ -119,6 +122,10 @@ RemoveOldAgent
echo "Downloading tactical agent..."
wget -q -O ${agentBin} "${agentDL}"
if [ $? -ne 0 ]; then
echo "ERROR: Unable to download tactical agent"
exit 1
fi
chmod +x ${agentBin}
MESH_NODE_ID=""
@@ -133,7 +140,7 @@ else
InstallMesh
sleep 2
echo "Getting mesh node id..."
MESH_NODE_ID=$(${agentBin} -m nixmeshnodeid)
MESH_NODE_ID=$(env XAUTHORITY=foo DISPLAY=bar ${agentBin} -m nixmeshnodeid)
fi
if [ ! -d "${agentBinPath}" ]; then
@@ -178,4 +185,4 @@ EOF
echo "${tacticalsvc}" | tee ${agentSysD} > /dev/null
systemctl daemon-reload
systemctl enable --now ${agentSvcName}
systemctl enable --now ${agentSvcName}

View File

@@ -1,4 +1,5 @@
import asyncio
from contextlib import suppress
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
@@ -24,15 +25,13 @@ class DashInfo(AsyncJsonWebsocketConsumer):
async def disconnect(self, close_code):
try:
with suppress(Exception):
self.dash_info.cancel()
except:
pass
self.connected = False
await self.close()
async def receive(self, json_data=None):
async def receive_json(self, payload, **kwargs):
pass
@database_sync_to_async

View File

@@ -13,7 +13,7 @@ $apilink = $downloadlink.split('/')
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$serviceName = 'tacticalagent'
$serviceName = 'tacticalrmm'
If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
write-host ('Tactical RMM Is Already Installed')
} Else {
@@ -39,9 +39,8 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
$DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled
if ($DefenderStatus -match "True") {
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
Add-MpPreference -ExclusionPath 'C:\ProgramData\TacticalRMM\*'
}
}
Catch {

View File

@@ -0,0 +1,16 @@
#!/bin/bash
if [ -f /usr/local/mesh_services/meshagent/meshagent ]; then
/usr/local/mesh_services/meshagent/meshagent -fulluninstall
fi
if [ -f /opt/tacticalmesh/meshagent ]; then
/opt/tacticalmesh/meshagent -fulluninstall
fi
launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist
rm -rf /usr/local/mesh_services
rm -rf /opt/tacticalmesh
rm -f /etc/tacticalagent
rm -rf /opt/tacticalagent
rm -f /Library/LaunchDaemons/tacticalagent.plist

View File

@@ -0,0 +1,60 @@
import configparser
import os
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Generate conf for uwsgi"
def handle(self, *args, **kwargs):
self.stdout.write("Creating uwsgi conf...")
config = configparser.ConfigParser()
if getattr(settings, "DOCKER_BUILD", False):
home = str(Path(os.getenv("VIRTUAL_ENV"))) # type: ignore
socket = "0.0.0.0:8080"
else:
home = str(settings.BASE_DIR.parents[0] / "env")
socket = str(settings.BASE_DIR / "tacticalrmm.sock")
config["uwsgi"] = {
"chdir": str(settings.BASE_DIR),
"module": "tacticalrmm.wsgi",
"home": home,
"master": str(getattr(settings, "UWSGI_MASTER", True)).lower(),
"enable-threads": str(
getattr(settings, "UWSGI_ENABLE_THREADS", True)
).lower(),
"socket": socket,
"harakiri": str(getattr(settings, "UWSGI_HARAKIRI", 300)),
"chmod-socket": str(getattr(settings, "UWSGI_CHMOD_SOCKET", 660)),
"buffer-size": str(getattr(settings, "UWSGI_BUFFER_SIZE", 65535)),
"vacuum": str(getattr(settings, "UWSGI_VACUUM", True)).lower(),
"die-on-term": str(getattr(settings, "UWSGI_DIE_ON_TERM", True)).lower(),
"max-requests": str(getattr(settings, "UWSGI_MAX_REQUESTS", 500)),
"disable-logging": str(
getattr(settings, "UWSGI_DISABLE_LOGGING", True)
).lower(),
"cheaper-algo": "busyness",
"cheaper": str(getattr(settings, "UWSGI_CHEAPER", 4)),
"cheaper-initial": str(getattr(settings, "UWSGI_CHEAPER_INITIAL", 4)),
"workers": str(getattr(settings, "UWSGI_MAX_WORKERS", 40)),
"cheaper-step": str(getattr(settings, "UWSGI_CHEAPER_STEP", 2)),
"cheaper-overload": str(getattr(settings, "UWSGI_CHEAPER_OVERLOAD", 3)),
"cheaper-busyness-min": str(getattr(settings, "UWSGI_BUSYNESS_MIN", 5)),
"cheaper-busyness-max": str(getattr(settings, "UWSGI_BUSYNESS_MAX", 10)),
}
if getattr(settings, "UWSGI_DEBUG", False):
config["uwsgi"]["stats"] = "/tmp/stats.socket"
config["uwsgi"]["cheaper-busyness-verbose"] = str(True).lower()
with open(settings.BASE_DIR / "app.ini", "w") as fp:
config.write(fp)
self.stdout.write("Created uwsgi conf")

View File

@@ -1,3 +1,5 @@
from urllib.parse import urlparse
from django.conf import settings
from django.core.management.base import BaseCommand
@@ -22,6 +24,8 @@ class Command(BaseCommand):
self.stdout.write(settings.NATS_SERVER_VER)
case "frontend":
self.stdout.write(settings.CORS_ORIGIN_WHITELIST[0])
case "webdomain":
self.stdout.write(urlparse(settings.CORS_ORIGIN_WHITELIST[0]).netloc)
case "djangoadmin":
url = f"https://{settings.ALLOWED_HOSTS[0]}/{settings.ADMIN_URL}"
self.stdout.write(url)
@@ -39,7 +43,7 @@ class Command(BaseCommand):
self.stdout.write(settings.DATABASES["default"]["HOST"])
case "dbport":
self.stdout.write(settings.DATABASES["default"]["PORT"])
case "meshsite" | "meshuser" | "meshtoken":
case "meshsite" | "meshuser" | "meshtoken" | "meshdomain":
from core.models import CoreSettings
core: "CoreSettings" = CoreSettings.objects.first()
@@ -47,6 +51,8 @@ class Command(BaseCommand):
obj = core.mesh_site
elif kwargs["name"] == "meshuser":
obj = core.mesh_username
elif kwargs["name"] == "meshdomain":
obj = urlparse(core.mesh_site).netloc
else:
obj = core.mesh_token

View File

@@ -1,3 +1,5 @@
from contextlib import suppress
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
@@ -8,9 +10,7 @@ class Command(BaseCommand):
help = "Populates the global site settings on first install"
def handle(self, *args, **kwargs):
try:
# can only be 1 instance of this. Prevents error when rebuilding docker container
with suppress(ValidationError):
CoreSettings().save()
self.stdout.write("Core db populated")
except ValidationError:
# can only be 1 instance of this. Prevents error when rebuilding docker container
pass

View File

@@ -7,6 +7,6 @@ class Command(BaseCommand):
help = "Collection of tasks to run after updating the rmm, before migrations"
def handle(self, *args, **kwargs):
self.stdout.write(self.style.WARNING("Clearning the cache"))
self.stdout.write(self.style.WARNING("Cleaning the cache"))
clear_entire_cache()
self.stdout.write(self.style.SUCCESS("Cache was cleared!"))

View File

@@ -0,0 +1,630 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0034_alter_customfield_name"),
]
operations = [
migrations.AlterField(
model_name="coresettings",
name="default_time_zone",
field=models.CharField(
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"),
],
default="America/Los_Angeles",
max_length=255,
),
),
]

View File

@@ -1,4 +1,5 @@
import smtplib
from contextlib import suppress
from email.message import EmailMessage
from typing import TYPE_CHECKING, List, Optional, cast
@@ -108,12 +109,10 @@ class CoreSettings(BaseAuditModel):
# for install script
if not self.pk:
try:
with suppress(Exception):
self.mesh_site = settings.MESH_SITE
self.mesh_username = settings.MESH_USERNAME.lower()
self.mesh_token = settings.MESH_TOKEN_KEY
except:
pass
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
@@ -127,10 +126,10 @@ class CoreSettings(BaseAuditModel):
cache_agents_alert_template.delay()
if old_settings.workstation_policy != self.workstation_policy:
cache.delete_many_pattern(f"site_workstation_*")
cache.delete_many_pattern("site_workstation_*")
if old_settings.server_policy != self.server_policy:
cache.delete_many_pattern(f"site_server_*")
cache.delete_many_pattern("site_server_*")
if (
old_settings.server_policy != self.server_policy
@@ -182,10 +181,10 @@ class CoreSettings(BaseAuditModel):
test: bool = False,
) -> tuple[str, bool]:
if test and not self.email_is_configured:
return ("There needs to be at least one email recipient configured", False)
return "There needs to be at least one email recipient configured", False
# return since email must be configured to continue
elif not self.email_is_configured:
return ("SMTP messaging not configured.", False)
return "SMTP messaging not configured.", False
# override email from if alert_template is passed and is set
if alert_template and alert_template.email_from:
@@ -199,7 +198,7 @@ class CoreSettings(BaseAuditModel):
elif self.email_alert_recipients:
email_recipients = ", ".join(cast(List[str], self.email_alert_recipients))
else:
return ("There needs to be at least one email recipient configured", False)
return "There needs to be at least one email recipient configured", False
try:
msg = EmailMessage()
@@ -226,12 +225,12 @@ class CoreSettings(BaseAuditModel):
except Exception as e:
DebugLog.error(message=f"Sending email failed with error: {e}")
if test:
return (str(e), False)
return str(e), False
if test:
return ("Email test ok!", True)
return "Email test ok!", True
return ("ok", True)
return "ok", True
def send_sms(
self,
@@ -240,7 +239,7 @@ class CoreSettings(BaseAuditModel):
test: bool = False,
) -> tuple[str, bool]:
if not self.sms_is_configured:
return ("Sms alerting is not setup correctly.", False)
return "Sms alerting is not setup correctly.", False
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients:
@@ -248,7 +247,7 @@ class CoreSettings(BaseAuditModel):
elif self.sms_alert_recipients:
text_recipients = cast(List[str], self.sms_alert_recipients)
else:
return ("No sms recipients found", False)
return "No sms recipients found", False
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in text_recipients:
@@ -257,12 +256,12 @@ class CoreSettings(BaseAuditModel):
except TwilioRestException as e:
DebugLog.error(message=f"SMS failed to send: {e}")
if test:
return (str(e), False)
return str(e), False
if test:
return ("SMS Test sent successfully!", True)
return "SMS Test sent successfully!", True
return ("ok", True)
return "ok", True
@staticmethod
def serialize(core):
@@ -315,8 +314,8 @@ class CustomField(BaseAuditModel):
return self.default_values_multiple
elif self.type == CustomFieldType.CHECKBOX:
return self.default_value_bool
else:
return self.default_value_string
return self.default_value_string
def get_or_create_field_value(self, instance):
from agents.models import Agent, AgentCustomField
@@ -365,6 +364,23 @@ class CodeSignToken(models.Model):
return r.status_code == 200
@property
def is_expired(self) -> bool:
if not self.token:
return False
try:
r = requests.post(
settings.CHECK_TOKEN_URL,
json={"token": self.token, "api": settings.ALLOWED_HOSTS[0]},
headers={"Content-type": "application/json"},
timeout=15,
)
except:
return False
return r.status_code == 401
def __str__(self):
return "Code signing token"

View File

@@ -7,8 +7,8 @@ class CoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_core_settings")
else:
return _has_perm(r, "can_edit_core_settings")
return _has_perm(r, "can_edit_core_settings")
class URLActionPerms(permissions.BasePermission):
@@ -30,5 +30,5 @@ class CustomFieldPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_customfields")
else:
return _has_perm(r, "can_manage_customfields")
return _has_perm(r, "can_manage_customfields")

View File

@@ -174,7 +174,7 @@ def _get_failing_data(agents: "QuerySet[Any]") -> Dict[str, bool]:
and task.task_result.status == TaskStatus.FAILING
and task.alert_severity == AlertSeverity.WARNING
):
data["warning"]
data["warning"] = True
return data

View File

@@ -5,13 +5,20 @@ from channels.db import database_sync_to_async
from channels.testing import WebsocketCommunicator
from django.conf import settings
from django.core.management import call_command
from django.test import override_settings
from model_bakery import baker
from rest_framework.authtoken.models import Token
from agents.models import Agent
from core.utils import get_core_settings
from core.utils import get_core_settings, get_meshagent_url
from logs.models import PendingAction
from tacticalrmm.constants import CONFIG_MGMT_CMDS, CustomFieldModel, PAAction, PAStatus
from tacticalrmm.constants import (
CONFIG_MGMT_CMDS,
CustomFieldModel,
MeshAgentIdent,
PAAction,
PAStatus,
)
from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo
@@ -104,7 +111,7 @@ class TestCoreTasks(TacticalTestCase):
url = "/core/settings/"
# setup
policies = baker.make("automation.Policy", _quantity=2)
baker.make("automation.Policy", _quantity=2)
# test normal request
data = {
"smtp_from_email": "newexample@example.com",
@@ -122,7 +129,7 @@ class TestCoreTasks(TacticalTestCase):
def test_ui_maintenance_actions(self, remove_orphaned_win_tasks, reload_nats):
url = "/core/servermaintenance/"
agents = baker.make_recipe("agents.online_agent", _quantity=3)
baker.make_recipe("agents.online_agent", _quantity=3)
# test with empty data
r = self.client.post(url, {})
@@ -179,9 +186,7 @@ class TestCoreTasks(TacticalTestCase):
url = "/core/customfields/"
# setup
custom_fields = baker.make(
"core.CustomField", model=CustomFieldModel.AGENT, _quantity=5
)
baker.make("core.CustomField", model=CustomFieldModel.AGENT, _quantity=5)
baker.make("core.CustomField", model="client", _quantity=5)
# will error if request invalid
@@ -190,7 +195,6 @@ class TestCoreTasks(TacticalTestCase):
data = {"model": "agent"}
r = self.client.patch(url, data)
serializer = CustomFieldSerializer(custom_fields, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 5)
@@ -444,3 +448,58 @@ class TestCorePermissions(TacticalTestCase):
def setUp(self):
self.setup_client()
self.setup_coresettings()
class TestCoreUtils(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_get_meshagent_url_standard(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=4&meshid=abc123&installflags=0",
)
@override_settings(DOCKER_BUILD=True)
@override_settings(MESH_WS_URL="ws://tactical-meshcentral:4443")
def test_get_meshagent_url_docker(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=4&meshid=abc123&installflags=0",
)

View File

@@ -1,6 +1,7 @@
import json
import subprocess
import tempfile
import urllib.parse
from base64 import b64encode
from typing import TYPE_CHECKING, Optional, cast
@@ -11,10 +12,16 @@ from django.core.cache import cache
from django.http import FileResponse
from meshctrl.utils import get_auth_token
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, ROLE_CACHE_PREFIX
from tacticalrmm.constants import (
AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX,
CORESETTINGS_CACHE_KEY,
ROLE_CACHE_PREFIX,
AgentPlat,
MeshAgentIdent,
)
if TYPE_CHECKING:
from core.models import CodeSignToken, CoreSettings
from core.models import CoreSettings
class CoreSettingsNotFound(Exception):
@@ -23,6 +30,7 @@ class CoreSettingsNotFound(Exception):
def clear_entire_cache() -> None:
cache.delete_many_pattern(f"{ROLE_CACHE_PREFIX}*")
cache.delete_many_pattern(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}*")
cache.delete(CORESETTINGS_CACHE_KEY)
cache.delete_many_pattern("site_*")
cache.delete_many_pattern("agent_*")
@@ -47,6 +55,16 @@ def token_is_valid() -> tuple[str, bool]:
return "", False
def token_is_expired() -> bool:
from core.models import CodeSignToken
t: "CodeSignToken" = CodeSignToken.objects.first()
if not t or not t.token:
return False
return t.is_expired
def get_core_settings() -> "CoreSettings":
from core.models import CORESETTINGS_CACHE_KEY, CoreSettings
@@ -142,3 +160,28 @@ def sysd_svc_is_running(svc: str) -> bool:
cmd = ["systemctl", "is-active", "--quiet", svc]
r = subprocess.run(cmd, capture_output=True)
return not r.returncode
def get_meshagent_url(
*, ident: "MeshAgentIdent", plat: str, mesh_site: str, mesh_device_id: str
) -> str:
if settings.DOCKER_BUILD:
base = settings.MESH_WS_URL.replace("ws://", "http://")
else:
base = mesh_site
if plat == AgentPlat.WINDOWS:
params = {
"id": ident,
"meshid": mesh_device_id,
"installflags": 0,
}
else:
params = {
"id": mesh_device_id,
"installflags": 2,
"meshinstall": ident,
}
return base + "/meshagents?" + urllib.parse.urlencode(params)

View File

@@ -1,4 +1,5 @@
import re
from pathlib import Path
import psutil
import pytz
@@ -6,8 +7,8 @@ from cryptography import x509
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone as djangotime
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
@@ -73,6 +74,7 @@ def clear_cache(request):
@api_view()
def dashboard_info(request):
from core.utils import token_is_expired
from tacticalrmm.utils import get_latest_trmm_ver
return Response(
@@ -93,6 +95,7 @@ def dashboard_info(request):
"hosted": getattr(settings, "HOSTED", False),
"date_format": request.user.date_format,
"default_date_format": get_core_settings().date_format,
"token_is_expired": token_is_expired(),
}
)
@@ -175,8 +178,8 @@ class GetAddCustomFields(APIView):
if "model" in request.data.keys():
fields = CustomField.objects.filter(model=request.data["model"])
return Response(CustomFieldSerializer(fields, many=True).data)
else:
return notify_error("The request was invalid")
return notify_error("The request was invalid")
def post(self, request):
serializer = CustomFieldSerializer(data=request.data, partial=True)
@@ -231,7 +234,7 @@ class CodeSign(APIView):
except Exception as e:
return notify_error(str(e))
if r.status_code == 400 or r.status_code == 401:
if r.status_code in (400, 401):
return notify_error(r.json()["ret"])
elif r.status_code == 200:
t = CodeSignToken.objects.first()
@@ -414,8 +417,7 @@ def status(request):
mem_usage: int = round(psutil.virtual_memory().percent)
cert_file, _ = get_certs()
with open(cert_file, "rb") as f:
cert_bytes = f.read()
cert_bytes = Path(cert_file).read_bytes()
cert = x509.load_pem_x509_certificate(cert_bytes)
expires = pytz.utc.localize(cert.not_valid_after)
@@ -424,6 +426,7 @@ def status(request):
ret = {
"version": settings.TRMM_VERSION,
"latest_agent_version": settings.LATEST_AGENT_VER,
"agent_count": Agent.objects.count(),
"client_count": Client.objects.count(),
"site_count": Site.objects.count(),

View File

@@ -5,4 +5,4 @@ class LogsConfig(AppConfig):
name = "logs"
def ready(self):
from . import signals
from . import signals # noqa

View File

@@ -282,7 +282,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [DebugLogLevel.INFO]:
if get_debug_level() == DebugLogLevel.INFO:
cls.objects.create(
log_level=DebugLogLevel.INFO,
agent=agent,
@@ -297,7 +297,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [DebugLogLevel.INFO, DebugLogLevel.WARN]:
if get_debug_level() in (DebugLogLevel.INFO, DebugLogLevel.WARN):
cls.objects.create(
log_level=DebugLogLevel.INFO,
agent=agent,
@@ -312,11 +312,11 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [
if get_debug_level() in (
DebugLogLevel.INFO,
DebugLogLevel.WARN,
DebugLogLevel.ERROR,
]:
):
cls.objects.create(
log_level=DebugLogLevel.ERROR,
agent=agent,
@@ -331,12 +331,12 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [
if get_debug_level() in (
DebugLogLevel.INFO,
DebugLogLevel.WARN,
DebugLogLevel.ERROR,
DebugLogLevel.CRITICAL,
]:
):
cls.objects.create(
log_level=DebugLogLevel.CRITICAL,
agent=agent,
@@ -376,8 +376,8 @@ class PendingAction(models.Model):
return "Next update cycle"
elif self.action_type == PAAction.CHOCO_INSTALL:
return "ASAP"
else:
return "On next checkin"
return "On next checkin"
@property
def description(self) -> Optional[str]:
@@ -390,15 +390,15 @@ class PendingAction(models.Model):
elif self.action_type == PAAction.CHOCO_INSTALL:
return f"{self.details['name']} software install"
elif self.action_type in [
elif self.action_type in (
PAAction.RUN_CMD,
PAAction.RUN_SCRIPT,
PAAction.RUN_PATCH_SCAN,
PAAction.RUN_PATCH_INSTALL,
]:
):
return f"{self.action_type}"
else:
return None
return None
class BaseAuditModel(models.Model):

View File

@@ -16,8 +16,8 @@ class AuditLogSerializer(serializers.ModelSerializer):
return SiteMinimumSerializer(
Agent.objects.get(agent_id=obj.agent_id).site
).data
else:
return None
return None
class Meta:
model = AuditLog

View File

@@ -1,11 +1,10 @@
import datetime as dt
import pytz
from django.db.models.signals import post_init
from django.dispatch import receiver
from django.utils import timezone as djangotime
from tacticalrmm.constants import PAAction, PAStatus
from tacticalrmm.helpers import date_is_in_past
from .models import PendingAction
@@ -22,14 +21,8 @@ def handle_status(sender, instance: PendingAction, **kwargs):
reboot_time = dt.datetime.strptime(
instance.details["time"], "%Y-%m-%d %H:%M:%S"
)
# need to convert agent tz to UTC in order to compare
agent_tz = pytz.timezone(instance.agent.timezone)
localized = agent_tz.localize(reboot_time)
now = djangotime.now()
reboot_time_utc = localized.astimezone(pytz.utc)
if now > reboot_time_utc:
if date_is_in_past(
datetime_obj=reboot_time, agent_tz=instance.agent.timezone
):
instance.status = PAStatus.COMPLETED
instance.save(update_fields=["status"])

View File

@@ -250,7 +250,7 @@ class TestAuditViews(TacticalTestCase):
_quantity=4,
)
logs = baker.make(
logs = baker.make( # noqa
"logs.DebugLog",
log_type=DebugLogType.SYSTEM_ISSUES,
log_level=cycle([i.value for i in DebugLogLevel]),
@@ -391,8 +391,8 @@ class TestAuditViews(TacticalTestCase):
def test_get_pendingaction_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
actions = baker.make("logs.PendingAction", agent=agent, _quantity=5)
unauthorized_actions = baker.make(
actions = baker.make("logs.PendingAction", agent=agent, _quantity=5) # noqa
unauthorized_actions = baker.make( # noqa
"logs.PendingAction", agent=unauthorized_agent, _quantity=7
)

View File

@@ -4,7 +4,7 @@ django-extensions
isort
types-pytz
django-silk
mypy
mypy==0.982
django-stubs
djangorestframework-stubs
django-types

View File

@@ -5,4 +5,6 @@ pytest
pytest-django
pytest-xdist
pytest-cov
codecov
codecov
refurb
flake8

View File

@@ -1,39 +1,39 @@
asgiref==3.5.2
celery==5.2.7
certifi==2022.6.15
certifi==2022.9.24
cffi==1.15.1
channels==3.0.5
channels_redis==3.4.0
channels==4.0.0
channels_redis==4.0.0
chardet==4.0.0
cryptography==37.0.4
daphne==3.0.2
Django==4.0.6
cryptography==38.0.4
daphne==4.0.0
Django==4.1.3
django-cors-headers==3.13.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
djangorestframework==3.14.0
drf-spectacular==0.24.2
future==0.18.2
msgpack==1.0.4
nats-py==2.1.3
psutil==5.9.1
psycopg2-binary==2.9.3
pycparser==2.21
pycryptodome==3.15.0
pyotp==2.6.0
pyparsing==3.0.9
pytz==2022.1
qrcode==7.3.1
redis==4.3.4
hiredis==2.0.0
meshctrl==0.1.15
msgpack==1.0.4
nats-py==2.2.0
psutil==5.9.4
psycopg2-binary==2.9.5
pycparser==2.21
pycryptodome==3.16.0
pyotp==2.7.0
pyparsing==3.0.9
pytz==2022.5
qrcode==7.3.1
redis==4.3.5
requests==2.28.1
six==1.16.0
sqlparse==0.4.2
twilio==7.10.0
urllib3==1.26.9
uWSGI==2.0.20
sqlparse==0.4.3
twilio==7.15.4
urllib3==1.26.13
uWSGI==2.0.21
validators==0.20.0
vine==5.0.0
websockets==10.3
zipp==3.8.0
drf_spectacular==0.22.1
meshctrl==0.1.15
websockets==10.4
zipp==3.11.0

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-30 21:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0017_auto_20220311_0100'),
]
operations = [
migrations.AddField(
model_name='script',
name='run_as_user',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.1.3 on 2022-11-26 01:38
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("scripts", "0018_script_run_as_user"),
]
operations = [
migrations.AddField(
model_name="script",
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,
),
),
]

View File

@@ -29,6 +29,12 @@ class Script(BaseAuditModel):
blank=True,
default=list,
)
env_vars = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
syntax = TextField(null=True, blank=True)
favorite = models.BooleanField(default=False)
category = models.CharField(max_length=100, null=True, blank=True)
@@ -40,13 +46,14 @@ class Script(BaseAuditModel):
supported_platforms = ArrayField(
models.CharField(max_length=20), null=True, blank=True, default=list
)
run_as_user = models.BooleanField(default=False)
def __str__(self):
return self.name
@property
def code_no_snippets(self):
return self.script_body if self.script_body else ""
return self.script_body or ""
@property
def code(self):
@@ -65,11 +72,12 @@ class Script(BaseAuditModel):
else:
value = ""
replaced_code = re.sub(snippet.group(), value, replaced_code)
replaced_code = re.sub(
snippet.group(), value.replace("\\", "\\\\"), replaced_code
)
return replaced_code
else:
return code
return code
def hash_script_body(self):
from django.conf import settings
@@ -111,14 +119,14 @@ class Script(BaseAuditModel):
else 90
)
args = script["args"] if "args" in script.keys() else list()
args = script["args"] if "args" in script.keys() else []
syntax = script["syntax"] if "syntax" in script.keys() else ""
supported_platforms = (
script["supported_platforms"]
if "supported_platforms" in script.keys()
else list()
else []
)
# if community script exists update it
@@ -186,12 +194,12 @@ class Script(BaseAuditModel):
return ScriptSerializer(script).data
@classmethod
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list:
def parse_script_args(cls, agent, shell: str, args: List[str] = []) -> list:
if not args:
return []
temp_args = list()
temp_args = []
# pattern to match for injection
pattern = re.compile(".*\\{\\{(.*)\\}\\}.*")
@@ -205,7 +213,7 @@ class Script(BaseAuditModel):
string=string,
instance=agent,
shell=shell,
quotes=True if shell != ScriptShell.CMD else False,
quotes=shell != ScriptShell.CMD,
)
if value:

View File

@@ -7,5 +7,5 @@ class ScriptsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_scripts")
else:
return _has_perm(r, "can_manage_scripts")
return _has_perm(r, "can_manage_scripts")

Some files were not shown because too many files have changed in this diff Show More