Compare commits

...

162 Commits

Author SHA1 Message Date
wh1te909
01ee524049 Release 0.4.3 2021-01-30 04:45:10 +00:00
wh1te909
af9cb65338 bump version 2021-01-30 04:44:41 +00:00
wh1te909
8aa11c580b move agents monitor task to go 2021-01-30 04:39:15 +00:00
wh1te909
ada627f444 forgot to enable natsapi during install 2021-01-30 04:28:27 +00:00
wh1te909
a7b6d338c3 update reqs 2021-01-30 02:06:56 +00:00
wh1te909
9f00538b97 fix tests 2021-01-29 23:38:59 +00:00
wh1te909
a085015282 increase timeout for security eventlogs 2021-01-29 23:34:16 +00:00
wh1te909
0b9c220fbb remove old task 2021-01-29 20:36:28 +00:00
wh1te909
0e3d04873d move wmi celery task to golang 2021-01-29 20:10:52 +00:00
wh1te909
b7578d939f add test for community script shell type 2021-01-29 09:37:34 +00:00
wh1te909
b5c28de03f Release 0.4.2 2021-01-29 08:23:06 +00:00
wh1te909
e17d25c156 bump versions 2021-01-29 08:12:03 +00:00
wh1te909
c25dc1b99c also override shell during load community scripts 2021-01-29 07:39:08 +00:00
Tragic Bronson
a493a574bd Merge pull request #265 from saulens22/patch-1
Fix "TRMM Defender Exclusions" script shell type
2021-01-28 23:36:03 -08:00
Saulius Kazokas
4284493dce Fix "TRMM Defender Exclusions" script shell type 2021-01-29 07:10:10 +02:00
wh1te909
25059de8e1 fix superseded windows defender updates 2021-01-29 02:37:51 +00:00
wh1te909
1731b05ad0 remove old serializers 2021-01-29 02:25:31 +00:00
wh1te909
e80dc663ac remove unused func 2021-01-29 02:22:06 +00:00
wh1te909
39988a4c2f cleanup an old view 2021-01-29 02:15:27 +00:00
wh1te909
415bff303a add some debug for unsupported agents 2021-01-29 01:22:35 +00:00
wh1te909
a65eb62a54 checkrunner changes wh1te909/rmmagent@10a0935f1b 2021-01-29 00:34:18 +00:00
wh1te909
03b2982128 update build flags 2021-01-28 23:11:32 +00:00
wh1te909
bff0527857 Release 0.4.1 2021-01-27 07:48:14 +00:00
wh1te909
f3b7634254 fix tests 2021-01-27 07:45:00 +00:00
wh1te909
6a9593c0b9 bump versions 2021-01-27 07:35:11 +00:00
wh1te909
edb785b8e5 prepare for agent 1.4.0 2021-01-27 07:11:49 +00:00
wh1te909
26d757b50a checkrunner interval changes wh1te909/rmmagent@7f131d54cf 2021-01-27 06:38:42 +00:00
wh1te909
535079ee87 update natsapi 2021-01-26 20:54:30 +00:00
wh1te909
ac380c29c1 fix last response sorting closes #258 2021-01-26 19:58:08 +00:00
wh1te909
3fd212f26c more optimizations 2021-01-25 21:05:59 +00:00
wh1te909
04a3abc651 fix tests 2021-01-25 20:46:22 +00:00
wh1te909
6caf85ddd1 optimize some queries 2021-01-25 20:27:20 +00:00
wh1te909
16e4071508 use error msg from backend 2021-01-25 19:57:50 +00:00
wh1te909
69e7c4324b start mkdocs 2021-01-25 19:55:48 +00:00
wh1te909
a1c4a8cbe5 fix tab refresh 2021-01-23 06:27:33 +00:00
wh1te909
e37f6cfda7 Release 0.4.0 2021-01-23 03:46:22 +00:00
wh1te909
989c804409 bump version 2021-01-23 03:45:49 +00:00
sadnub
7345bc3c82 fix image build script 2021-01-22 20:04:30 -05:00
sadnub
69bee35700 remove winupdate container from dev 2021-01-22 20:03:30 -05:00
sadnub
598e24df7c remove salt and celery-winupdate containers 2021-01-22 19:57:58 -05:00
sadnub
0ae669201e Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-22 19:26:03 -05:00
wh1te909
f52a8a4642 black 2021-01-23 00:02:26 +00:00
wh1te909
9c40b61ef2 fix test 2021-01-22 23:41:10 +00:00
wh1te909
72dabcda83 fix a test 2021-01-22 23:29:18 +00:00
wh1te909
161a06dbcc don't change tab when using site refresh button 2021-01-22 23:27:28 +00:00
wh1te909
8ed3d4e70c update quasar 2021-01-22 23:26:44 +00:00
wh1te909
a4223ccc8a bump agent and mesh vers 2021-01-22 22:56:33 +00:00
wh1te909
ca85923855 add purge 2021-01-22 09:34:08 +00:00
wh1te909
52bfe7c493 update natsapi 2021-01-22 00:41:27 +00:00
wh1te909
4786bd0cbe create meshusername during install 2021-01-22 00:40:09 +00:00
wh1te909
cadab160ff add check to remove salt 2021-01-21 23:58:31 +00:00
wh1te909
6a7f17b2b0 more salt cleanup 2021-01-21 00:00:34 +00:00
wh1te909
4986a4d775 more salt cleanup 2021-01-20 23:22:02 +00:00
wh1te909
903af0c2cf goodbye salt, you've served us well 2021-01-20 22:11:54 +00:00
wh1te909
3282fa803c move to go for chocolatey wh1te909/rmmagent@cebde22fa0 2021-01-19 23:43:37 +00:00
wh1te909
67cc47608d add hosts check to migration doc 2021-01-19 23:25:35 +00:00
wh1te909
0411704b8b update rmmagent and resty 2021-01-19 23:10:50 +00:00
wh1te909
1de85b2c69 more winupdate rework wh1te909/rmmagent@08ec2f9191 2021-01-19 03:14:54 +00:00
wh1te909
33b012f29d typo 2021-01-19 03:11:07 +00:00
wh1te909
1357584df3 start winupdate rework 2021-01-19 00:59:38 +00:00
sadnub
e15809e271 Merge branch 'develop' of https://github.com/sadnub/tacticalrmm into develop 2021-01-18 09:17:17 -05:00
wh1te909
0da1950427 Release 0.3.3 2021-01-18 11:01:25 +00:00
wh1te909
e590b921be fix #252 2021-01-18 11:00:50 +00:00
wh1te909
09462692f5 Release 0.3.2 2021-01-18 10:00:45 +00:00
wh1te909
c1d1b5f762 bump version 2021-01-18 10:00:26 +00:00
wh1te909
6b9c87b858 feat: set agent table tab default #249 2021-01-18 09:57:50 +00:00
wh1te909
485b6eb904 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-18 09:32:00 +00:00
wh1te909
057630bdb5 fix agent table sort #250 2021-01-18 09:31:28 +00:00
wh1te909
6b02873b30 fix agent table sort #250 2021-01-18 09:12:01 +00:00
wh1te909
0fa0fc6d6b add json linter to migration docs 2021-01-17 18:09:47 +00:00
wh1te909
339ec07465 Release 0.3.1 2021-01-17 05:48:27 +00:00
wh1te909
cd2e798fea bump versions 2021-01-17 05:43:34 +00:00
wh1te909
d5cadbeae2 split agent update into chunks 2021-01-17 05:42:38 +00:00
wh1te909
8046a3ccae Release 0.3.0 2021-01-17 02:16:06 +00:00
wh1te909
bf91d60b31 natsapi bin 1.0.0 2021-01-17 02:07:53 +00:00
wh1te909
539c047ec8 update go 2021-01-17 01:53:45 +00:00
wh1te909
290c18fa87 bump versions 2021-01-17 01:22:08 +00:00
wh1te909
98c46f5e57 fix domain 2021-01-17 01:21:21 +00:00
wh1te909
f8bd5b5b4e update configs/scripts and add migration docs for 0.3.0 2021-01-17 01:16:28 +00:00
wh1te909
816d32edad black 2021-01-16 23:34:55 +00:00
wh1te909
8453835c05 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-16 23:32:54 +00:00
wh1te909
9328c356c8 possible fix for mesh scaling 2021-01-16 23:32:46 +00:00
sadnub
89e3c1fc94 remove my print statements 2021-01-16 17:46:56 -05:00
sadnub
67e54cd15d Remove pending action duplicates and make policy check/task propogation more efficient 2021-01-16 17:46:56 -05:00
sadnub
278ea24786 improve dev env 2021-01-16 17:46:56 -05:00
sadnub
aba1662631 remove my print statements 2021-01-16 17:46:30 -05:00
sadnub
61eeb60c19 Remove pending action duplicates and make policy check/task propogation more efficient 2021-01-16 17:44:27 -05:00
wh1te909
5e9a8f4806 new natsapi binary 2021-01-16 21:55:06 +00:00
wh1te909
4cb274e9bc update to celery 5 2021-01-16 21:52:30 +00:00
wh1te909
8b9b1a6a35 update mesh docker conf 2021-01-16 21:50:29 +00:00
sadnub
2655964113 improve dev env 2021-01-16 11:20:24 -05:00
wh1te909
188bad061b add wmi task 2021-01-16 10:31:00 +00:00
wh1te909
3af4c329aa update reqs 2021-01-16 09:42:03 +00:00
wh1te909
6c13395f7d add debug 2021-01-16 09:41:27 +00:00
wh1te909
77b32ba360 remove import 2021-01-16 09:39:15 +00:00
sadnub
91dba291ac nats-api fixes 2021-01-15 23:41:21 -05:00
sadnub
a6bc293640 Finish up check charts 2021-01-15 22:11:40 -05:00
sadnub
53882d6e5f fix dev port 2021-01-15 21:25:32 -05:00
sadnub
d68adfbf10 docker nats-api rework 2021-01-15 21:11:27 -05:00
sadnub
498a392d7f check graphs wip 2021-01-15 21:10:25 -05:00
sadnub
740f6c05db docker cli additions 2021-01-15 21:10:25 -05:00
wh1te909
d810ce301f update natsapi flags 2021-01-16 00:01:31 +00:00
wh1te909
5ef6a14d24 add nats-api binary 2021-01-15 18:21:25 +00:00
wh1te909
a13f6f1e68 move recovery to natsapi 2021-01-15 10:19:01 +00:00
wh1te909
d2d0f1aaee fix tests 2021-01-15 09:57:46 +00:00
wh1te909
e64c72cc89 #234 sort proc mem using bytes wh1te909/rmmagent@04470dd4ce 2021-01-15 09:44:18 +00:00
wh1te909
9ab915a08b Release 0.2.23 2021-01-14 02:43:56 +00:00
wh1te909
e26fbf0328 bump versions 2021-01-14 02:29:14 +00:00
wh1te909
d9a52c4a2a update reqs 2021-01-14 02:27:40 +00:00
wh1te909
7b2ec90de9 feat: double-click agent action #232 2021-01-14 02:21:08 +00:00
wh1te909
d310bf8bbf add community scripts from dinger #242 2021-01-14 01:17:58 +00:00
wh1te909
2abc6cc939 partially fix sort 2021-01-14 00:01:08 +00:00
sadnub
56d4e694a2 fix annotations and error for the check chart 2021-01-13 18:43:09 -05:00
wh1te909
5f002c9cdc bump mesh 2021-01-13 23:35:14 +00:00
wh1te909
759daf4b4a add wording 2021-01-13 23:35:01 +00:00
wh1te909
3a8d9568e3 split some tasks into chunks to reduce load 2021-01-13 22:26:54 +00:00
wh1te909
ff22a9d94a fix deployments in docker 2021-01-13 22:19:09 +00:00
sadnub
a6e42d5374 fix removing pendingactions that are outstanding 2021-01-13 13:21:09 -05:00
wh1te909
a2f74e0488 add natsapi flags 2021-01-12 21:14:43 +00:00
wh1te909
ee44240569 black 2021-01-12 21:06:44 +00:00
wh1te909
d0828744a2 update nginx conf
(cherry picked from commit bf61e27f8a)
2021-01-12 06:38:52 +00:00
wh1te909
6e2e576b29 start natsapi 2021-01-12 06:32:00 +00:00
wh1te909
bf61e27f8a update nginx conf 2021-01-12 03:02:03 +00:00
Tragic Bronson
c441c30b46 Merge pull request #243 from sadnub/develop
Move Check Runs from Audit to its own table
2021-01-11 00:29:59 -08:00
Tragic Bronson
0e741230ea Merge pull request #242 from dinger1986/develop
Added some scripts checks etc
2021-01-11 00:29:47 -08:00
sadnub
1bfe9ac2db complete other pending actions with same task if task is deleted 2021-01-10 20:19:38 -05:00
sadnub
6812e72348 fix process sorting 2021-01-10 19:35:39 -05:00
sadnub
b6449d2f5b black 2021-01-10 16:33:10 -05:00
sadnub
7e3ea20dce add some tests and bug fixes 2021-01-10 16:27:48 -05:00
sadnub
c9d6fe9dcd allow returning all check data 2021-01-10 15:14:02 -05:00
sadnub
4a649a6b8b black 2021-01-10 14:47:34 -05:00
sadnub
8fef184963 add check history graph for cpu, memory, and diskspace 2021-01-10 14:15:05 -05:00
sadnub
69583ca3c0 docker dev fixes 2021-01-10 13:17:49 -05:00
dinger1986
6038a68e91 Win Defender exclusions for Tactical 2021-01-10 17:56:12 +00:00
dinger1986
fa8bd8db87 Manually reinstall Mesh just incase 2021-01-10 17:54:41 +00:00
dinger1986
18b4f0ed0f Runs DNS check on host as defined 2021-01-10 17:53:53 +00:00
dinger1986
461f9d66c9 Disable Faststartup on Windows 10 2021-01-10 17:51:33 +00:00
dinger1986
2155103c7a Check Win Defender for detections etc 2021-01-10 17:51:06 +00:00
dinger1986
c9a6839c45 Clears Win Defender log files 2021-01-10 17:50:13 +00:00
dinger1986
9fbe331a80 Allows the following Apps access by Win Defender 2021-01-10 17:49:36 +00:00
dinger1986
a56389c4ce Sync time with DC 2021-01-10 17:46:47 +00:00
dinger1986
64656784cb Powershell Speedtest 2021-01-10 17:46:00 +00:00
dinger1986
6eff2c181e Install RDP and change power config 2021-01-10 17:44:23 +00:00
dinger1986
1aa48c6d62 Install OpenSSH on PCs 2021-01-10 17:42:11 +00:00
dinger1986
c7ca1a346d Enable Windows Defender and set preferences 2021-01-10 17:40:06 +00:00
dinger1986
fa0ec7b502 check Duplicati Backup is running properly 2021-01-10 17:38:06 +00:00
dinger1986
768438c136 Checks disks for errors reported in event viewer 2021-01-10 17:36:42 +00:00
dinger1986
9badea0b3c Update DiskStatus.ps1
Checks local disks for errors reported in event viewer within the last 24 hours
2021-01-10 17:35:50 +00:00
dinger1986
43263a1650 Add files via upload 2021-01-10 17:33:48 +00:00
wh1te909
821e02dc75 update mesh docker conf 2021-01-10 00:20:44 +00:00
wh1te909
ed011ecf28 remove old mesh overrides #217 2021-01-10 00:15:11 +00:00
wh1te909
d861de4c2f update community scripts 2021-01-09 22:26:02 +00:00
Tragic Bronson
3a3b2449dc Merge pull request #241 from RVL-Solutions/develop
Create Windows10Upgrade.ps1
2021-01-09 14:12:05 -08:00
Ruben van Leusden
d2614406ca Create Windows10Upgrade.ps1
Shared by Kyt through Discord
2021-01-08 22:20:33 +01:00
Tragic Bronson
0798d098ae Merge pull request #238 from wh1te909/revert-235-master
Revert "Create Windows10Upgrade.ps1"
2021-01-08 10:38:33 -08:00
Tragic Bronson
dab7ddc2bb Revert "Create Windows10Upgrade.ps1" 2021-01-08 10:36:42 -08:00
Tragic Bronson
081a96e281 Merge pull request #235 from RVL-Solutions/master
Create Windows10Upgrade.ps1
2021-01-08 10:36:19 -08:00
wh1te909
a7dd881d79 Release 0.2.22 2021-01-08 18:16:17 +00:00
wh1te909
8134d5e24d remove threading 2021-01-08 18:15:55 +00:00
Ruben van Leusden
ba6756cd45 Create Windows10Upgrade.ps1 2021-01-06 23:19:14 +01:00
Tragic Bronson
5d8fce21ac Merge pull request #230 from wh1te909/dependabot/npm_and_yarn/web/axios-0.21.1
Bump axios from 0.21.0 to 0.21.1 in /web
2021-01-05 13:51:18 -08:00
dependabot[bot]
e7e4a5bcd4 Bump axios from 0.21.0 to 0.21.1 in /web
Bumps [axios](https://github.com/axios/axios) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.0...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-05 15:54:54 +00:00
155 changed files with 49348 additions and 44164 deletions

View File

@@ -23,5 +23,6 @@ POSTGRES_USER=postgres
POSTGRES_PASS=postgrespass
# DEV SETTINGS
APP_PORT=8080
API_PORT=8000
APP_PORT=8000
API_PORT=8080
HTTP_PROTOCOL=https

View File

@@ -7,8 +7,10 @@ services:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-api"]
environment:
API_PORT: ${API_PORT}
ports:
- 8000:8000
- "8000:${API_PORT}"
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
@@ -19,40 +21,30 @@ services:
app-dev:
image: node:12-alpine
ports:
- 8080:8080
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port 8080"
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:${APP_PORT}"
networks:
dev:
aliases:
- tactical-frontend
# salt master and api
salt-dev:
image: ${IMAGE_REPO}tactical-salt:${VERSION}
restart: always
volumes:
- tactical-data-dev:/opt/tactical
- salt-data-dev:/etc/salt
ports:
- "4505:4505"
- "4506:4506"
networks:
dev:
aliases:
- tactical-salt
# nats
nats-dev:
image: ${IMAGE_REPO}tactical-nats:${VERSION}
restart: always
environment:
API_HOST: ${API_HOST}
API_PORT: ${API_PORT}
DEV: 1
ports:
- "4222:4222"
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
networks:
dev:
aliases:
@@ -136,6 +128,8 @@ services:
MESH_USER: ${MESH_USER}
TRMM_USER: ${TRMM_USER}
TRMM_PASS: ${TRMM_PASS}
HTTP_PROTOCOL: ${HTTP_PROTOCOL}
APP_PORT: ${APP_PORT}
depends_on:
- postgres-dev
- meshcentral-dev
@@ -179,23 +173,6 @@ services:
- postgres-dev
- redis-dev
# container for celery winupdate tasks
celerywinupdate-dev:
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-celerywinupdate-dev"]
restart: always
networks:
- dev
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
depends_on:
- postgres-dev
- redis-dev
nginx-dev:
# container for tactical reverse proxy
image: ${IMAGE_REPO}tactical-nginx:${VERSION}
@@ -206,8 +183,8 @@ services:
MESH_HOST: ${MESH_HOST}
CERT_PUB_KEY: ${CERT_PUB_KEY}
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
APP_PORT: 8080
API_PORT: 8000
APP_PORT: ${APP_PORT}
API_PORT: ${API_PORT}
networks:
dev:
ipv4_address: 172.21.0.20
@@ -222,7 +199,6 @@ volumes:
postgres-data-dev:
mongo-dev-data:
mesh-data-dev:
salt-data-dev:
networks:
dev:

View File

@@ -9,8 +9,6 @@ set -e
: "${POSTGRES_USER:=tactical}"
: "${POSTGRES_PASS:=tactical}"
: "${POSTGRES_DB:=tacticalrmm}"
: "${SALT_HOST:=tactical-salt}"
: "${SALT_USER:=saltapi}"
: "${MESH_CONTAINER:=tactical-meshcentral}"
: "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}"
@@ -18,6 +16,9 @@ set -e
: "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}"
: "${HTTP_PROTOCOL:=http}"
: "${APP_PORT:=8080}"
: "${API_PORT:=8000}"
# Add python venv to path
export PATH="${VIRTUAL_ENV}/bin:$PATH"
@@ -47,14 +48,6 @@ function django_setup {
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
# write salt pass to tmp dir
if [ ! -f "${TACTICAL__DIR}/tmp/salt_pass" ]; then
SALT_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
echo "${SALT_PASS}" > ${TACTICAL_DIR}/tmp/salt_pass
else
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
fi
localvars="$(cat << EOF
SECRET_KEY = '${DJANGO_SEKRET}'
@@ -68,7 +61,7 @@ KEY_FILE = '/opt/tactical/certs/privkey.pem'
SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts'
ALLOWED_HOSTS = ['${API_HOST}']
ALLOWED_HOSTS = ['${API_HOST}', '*']
ADMIN_URL = 'admin/'
@@ -103,9 +96,6 @@ if not DEBUG:
)
})
SALT_USERNAME = '${SALT_USER}'
SALT_PASSWORD = '${SALT_PASS}'
SALT_HOST = '${SALT_HOST}'
MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
@@ -137,17 +127,16 @@ if [ "$1" = 'tactical-init-dev' ]; then
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# setup Python virtual env and install dependencies
python -m venv --copies ${VIRTUAL_ENV}
test -f ${VIRTUAL_ENV} && python -m venv --copies ${VIRTUAL_ENV}
pip install --no-cache-dir -r /requirements.txt
django_setup
# create .env file for frontend
webenv="$(cat << EOF
PROD_URL = "http://${API_HOST}:8000"
DEV_URL = "http://${API_HOST}:8000"
DEV_HOST = 0.0.0.0
DEV_PORT = 8080
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
APP_URL = https://${APP_HOST}
EOF
)"
echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null
@@ -161,22 +150,20 @@ EOF
fi
if [ "$1" = 'tactical-api' ]; then
cp ${WORKSPACE_DIR}/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
chmod +x /usr/local/bin/goversioninfo
check_tactical_ready
python manage.py runserver 0.0.0.0:8000
python manage.py runserver 0.0.0.0:${API_PORT}
fi
if [ "$1" = 'tactical-celery-dev' ]; then
check_tactical_ready
celery -A tacticalrmm worker -l debug
env/bin/celery -A tacticalrmm worker -l debug
fi
if [ "$1" = 'tactical-celerybeat-dev' ]; then
check_tactical_ready
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
celery -A tacticalrmm beat -l debug
fi
if [ "$1" = 'tactical-celerywinupdate-dev' ]; then
check_tactical_ready
celery -A tacticalrmm worker -Q wupdate -l debug
env/bin/celery -A tacticalrmm beat -l debug
fi

View File

@@ -57,16 +57,6 @@ jobs:
platforms: linux/amd64
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
- name: Build and Push Tactical Salt Image
uses: docker/build-push-action@v2
with:
context: .
push: true
pull: true
file: ./docker/containers/tactical-salt/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},tacticalrmm/tactical-salt:latest
- name: Build and Push Tactical Frontend Image
uses: docker/build-push-action@v2
with:

19
.vscode/settings.json vendored
View File

@@ -41,4 +41,23 @@
"**/*.zip": true
},
},
"go.useLanguageServer": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": false,
},
"editor.snippetSuggestions": "none",
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true,
}
}

View File

@@ -6,7 +6,7 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral)
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.xlawgaming.com/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
@@ -62,7 +62,6 @@ sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow proto tcp from any to any port 4505,4506
sudo ufw allow proto tcp from any to any port 4222
sudo ufw enable && sudo ufw reload
```

View File

@@ -1,457 +0,0 @@
from __future__ import absolute_import
import psutil
import os
import datetime
import zlib
import json
import base64
import wmi
import win32evtlog
import win32con
import win32evtlogutil
import winerror
from time import sleep
import requests
import subprocess
import random
import platform
ARCH = "64" if platform.machine().endswith("64") else "32"
PROGRAM_DIR = os.path.join(os.environ["ProgramFiles"], "TacticalAgent")
TAC_RMM = os.path.join(PROGRAM_DIR, "tacticalrmm.exe")
NSSM = os.path.join(PROGRAM_DIR, "nssm.exe" if ARCH == "64" else "nssm-x86.exe")
TEMP_DIR = os.path.join(os.environ["WINDIR"], "Temp")
SYS_DRIVE = os.environ["SystemDrive"]
PY_BIN = os.path.join(SYS_DRIVE, "\\salt", "bin", "python.exe")
SALT_CALL = os.path.join(SYS_DRIVE, "\\salt", "salt-call.bat")
def get_services():
# see https://github.com/wh1te909/tacticalrmm/issues/38
# for why I am manually implementing the svc.as_dict() method of psutil
ret = []
for svc in psutil.win_service_iter():
i = {}
try:
i["display_name"] = svc.display_name()
i["binpath"] = svc.binpath()
i["username"] = svc.username()
i["start_type"] = svc.start_type()
i["status"] = svc.status()
i["pid"] = svc.pid()
i["name"] = svc.name()
i["description"] = svc.description()
except Exception:
continue
else:
ret.append(i)
return ret
def run_python_script(filename, timeout, script_type="userdefined"):
# no longer used in agent version 0.11.0
file_path = os.path.join(TEMP_DIR, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
if script_type == "userdefined":
__salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path)
else:
__salt__["cp.get_file"](f"salt://scripts/{filename}", file_path)
return __salt__["cmd.run_all"](f"{PY_BIN} {file_path}", timeout=timeout)
def run_script(filepath, filename, shell, timeout, args=[], bg=False):
if shell == "powershell" or shell == "cmd":
if args:
return __salt__["cmd.script"](
source=filepath,
args=" ".join(map(lambda x: f'"{x}"', args)),
shell=shell,
timeout=timeout,
bg=bg,
)
else:
return __salt__["cmd.script"](
source=filepath, shell=shell, timeout=timeout, bg=bg
)
elif shell == "python":
file_path = os.path.join(TEMP_DIR, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
__salt__["cp.get_file"](filepath, file_path)
salt_cmd = "cmd.run_bg" if bg else "cmd.run_all"
if args:
a = " ".join(map(lambda x: f'"{x}"', args))
cmd = f"{PY_BIN} {file_path} {a}"
return __salt__[salt_cmd](cmd, timeout=timeout)
else:
return __salt__[salt_cmd](f"{PY_BIN} {file_path}", timeout=timeout)
def uninstall_agent():
remove_exe = os.path.join(PROGRAM_DIR, "unins000.exe")
__salt__["cmd.run_bg"]([remove_exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"])
return "ok"
def update_salt():
for p in psutil.process_iter():
with p.oneshot():
if p.name() == "tacticalrmm.exe" and "updatesalt" in p.cmdline():
return "running"
from subprocess import Popen, PIPE
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
cmd = [TAC_RMM, "-m", "updatesalt"]
p = Popen(
cmd,
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
close_fds=True,
creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
)
return p.pid
def run_manual_checks():
__salt__["cmd.run_bg"]([TAC_RMM, "-m", "runchecks"])
return "ok"
def install_updates():
for p in psutil.process_iter():
with p.oneshot():
if p.name() == "tacticalrmm.exe" and "winupdater" in p.cmdline():
return "running"
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "winupdater"])
def _wait_for_service(svc, status, retries=10):
attempts = 0
while 1:
try:
service = psutil.win_service_get(svc)
except psutil.NoSuchProcess:
stat = "fail"
attempts += 1
sleep(5)
else:
stat = service.status()
if stat != status:
attempts += 1
sleep(5)
else:
attempts = 0
if attempts == 0 or attempts > retries:
break
return stat
def agent_update_v2(inno, url):
# make sure another instance of the update is not running
# this function spawns 2 instances of itself (because we call it twice with salt run_bg)
# so if more than 2 running, don't continue as an update is already running
count = 0
for p in psutil.process_iter():
try:
with p.oneshot():
if "win_agent.agent_update_v2" in p.cmdline():
count += 1
except Exception:
continue
if count > 2:
return "already running"
sleep(random.randint(1, 20)) # don't flood the rmm
exe = os.path.join(TEMP_DIR, inno)
if os.path.exists(exe):
try:
os.remove(exe)
except:
pass
try:
r = requests.get(url, stream=True, timeout=600)
except Exception:
return "failed"
if r.status_code != 200:
return "failed"
with open(exe, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
ret = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=120)
tac = _wait_for_service(svc="tacticalagent", status="running")
if tac != "running":
subprocess.run([NSSM, "start", "tacticalagent"], timeout=30)
chk = _wait_for_service(svc="checkrunner", status="running")
if chk != "running":
subprocess.run([NSSM, "start", "checkrunner"], timeout=30)
return "ok"
def do_agent_update_v2(inno, url):
return __salt__["cmd.run_bg"](
[
SALT_CALL,
"win_agent.agent_update_v2",
f"inno={inno}",
f"url={url}",
"--local",
]
)
def agent_update(version, url):
# make sure another instance of the update is not running
# this function spawns 2 instances of itself so if more than 2 running,
# don't continue as an update is already running
count = 0
for p in psutil.process_iter():
try:
with p.oneshot():
if "win_agent.agent_update" in p.cmdline():
count += 1
except Exception:
continue
if count > 2:
return "already running"
sleep(random.randint(1, 60)) # don't flood the rmm
try:
r = requests.get(url, stream=True, timeout=600)
except Exception:
return "failed"
if r.status_code != 200:
return "failed"
exe = os.path.join(TEMP_DIR, f"winagent-v{version}.exe")
with open(exe, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
services = ("tacticalagent", "checkrunner")
for svc in services:
subprocess.run([NSSM, "stop", svc], timeout=120)
sleep(10)
r = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=300)
sleep(30)
for svc in services:
subprocess.run([NSSM, "start", svc], timeout=120)
return "ok"
def do_agent_update(version, url):
return __salt__["cmd.run_bg"](
[
SALT_CALL,
"win_agent.agent_update",
f"version={version}",
f"url={url}",
"--local",
]
)
class SystemDetail:
def __init__(self):
self.c = wmi.WMI()
self.comp_sys_prod = self.c.Win32_ComputerSystemProduct()
self.comp_sys = self.c.Win32_ComputerSystem()
self.memory = self.c.Win32_PhysicalMemory()
self.os = self.c.Win32_OperatingSystem()
self.base_board = self.c.Win32_BaseBoard()
self.bios = self.c.Win32_BIOS()
self.disk = self.c.Win32_DiskDrive()
self.network_adapter = self.c.Win32_NetworkAdapter()
self.network_config = self.c.Win32_NetworkAdapterConfiguration()
self.desktop_monitor = self.c.Win32_DesktopMonitor()
self.cpu = self.c.Win32_Processor()
self.usb = self.c.Win32_USBController()
def get_all(self, obj):
ret = []
for i in obj:
tmp = [
{j: getattr(i, j)}
for j in list(i.properties)
if getattr(i, j) is not None
]
ret.append(tmp)
return ret
def system_info():
info = SystemDetail()
return {
"comp_sys_prod": info.get_all(info.comp_sys_prod),
"comp_sys": info.get_all(info.comp_sys),
"mem": info.get_all(info.memory),
"os": info.get_all(info.os),
"base_board": info.get_all(info.base_board),
"bios": info.get_all(info.bios),
"disk": info.get_all(info.disk),
"network_adapter": info.get_all(info.network_adapter),
"network_config": info.get_all(info.network_config),
"desktop_monitor": info.get_all(info.desktop_monitor),
"cpu": info.get_all(info.cpu),
"usb": info.get_all(info.usb),
}
def local_sys_info():
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "sysinfo"])
def get_procs():
ret = []
# setup
for proc in psutil.process_iter():
with proc.oneshot():
proc.cpu_percent(interval=None)
# need time for psutil to record cpu percent
sleep(1)
for c, proc in enumerate(psutil.process_iter(), 1):
x = {}
with proc.oneshot():
if proc.pid == 0 or not proc.name():
continue
x["name"] = proc.name()
x["cpu_percent"] = proc.cpu_percent(interval=None) / psutil.cpu_count()
x["memory_percent"] = proc.memory_percent()
x["pid"] = proc.pid
x["ppid"] = proc.ppid()
x["status"] = proc.status()
x["username"] = proc.username()
x["id"] = c
ret.append(x)
return ret
def _compress_json(j):
return {
"wineventlog": base64.b64encode(
zlib.compress(json.dumps(j).encode("utf-8", errors="ignore"))
).decode("ascii", errors="ignore")
}
def get_eventlog(logtype, last_n_days):
start_time = datetime.datetime.now() - datetime.timedelta(days=last_n_days)
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
status_dict = {
win32con.EVENTLOG_AUDIT_FAILURE: "AUDIT_FAILURE",
win32con.EVENTLOG_AUDIT_SUCCESS: "AUDIT_SUCCESS",
win32con.EVENTLOG_INFORMATION_TYPE: "INFO",
win32con.EVENTLOG_WARNING_TYPE: "WARNING",
win32con.EVENTLOG_ERROR_TYPE: "ERROR",
0: "INFO",
}
computer = "localhost"
hand = win32evtlog.OpenEventLog(computer, logtype)
total = win32evtlog.GetNumberOfEventLogRecords(hand)
log = []
uid = 0
done = False
try:
while 1:
events = win32evtlog.ReadEventLog(hand, flags, 0)
for ev_obj in events:
uid += 1
# return once total number of events reach or we'll be stuck in an infinite loop
if uid >= total:
done = True
break
the_time = ev_obj.TimeGenerated.Format()
time_obj = datetime.datetime.strptime(the_time, "%c")
if time_obj < start_time:
done = True
break
computer = str(ev_obj.ComputerName)
src = str(ev_obj.SourceName)
evt_type = str(status_dict[ev_obj.EventType])
evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID))
evt_category = str(ev_obj.EventCategory)
record = str(ev_obj.RecordNumber)
msg = (
str(win32evtlogutil.SafeFormatMessage(ev_obj, logtype))
.replace("<", "")
.replace(">", "")
)
event_dict = {
"computer": computer,
"source": src,
"eventType": evt_type,
"eventID": evt_id,
"eventCategory": evt_category,
"message": msg,
"time": the_time,
"record": record,
"uid": uid,
}
log.append(event_dict)
if done:
break
except Exception:
pass
win32evtlog.CloseEventLog(hand)
return _compress_json(log)

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.4 on 2021-01-14 01:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0009_user_show_community_scripts"),
]
operations = [
migrations.AddField(
model_name="user",
name="agent_dblclick_action",
field=models.CharField(
choices=[
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
],
default="editagent",
max_length=50,
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.5 on 2021-01-18 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0010_user_agent_dblclick_action"),
]
operations = [
migrations.AddField(
model_name="user",
name="default_agent_tbl_tab",
field=models.CharField(
choices=[
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
],
default="server",
max_length=50,
),
),
]

View File

@@ -3,12 +3,30 @@ from django.contrib.auth.models import AbstractUser
from logs.models import BaseAuditModel
AGENT_DBLCLICK_CHOICES = [
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
]
AGENT_TBL_TAB_CHOICES = [
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
]
class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True)
totp_key = models.CharField(max_length=50, null=True, blank=True)
dark_mode = models.BooleanField(default=True)
show_community_scripts = models.BooleanField(default=True)
agent_dblclick_action = models.CharField(
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
)
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
agent = models.OneToOneField(
"agents.Agent",

View File

@@ -278,6 +278,14 @@ class TestUserAction(TacticalTestCase):
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {
"userui": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url)

View File

@@ -189,12 +189,17 @@ class UserUI(APIView):
def patch(self, request):
user = request.user
if "dark_mode" in request.data:
if "dark_mode" in request.data.keys():
user.dark_mode = request.data["dark_mode"]
user.save(update_fields=["dark_mode"])
if "show_community_scripts" in request.data:
if "show_community_scripts" in request.data.keys():
user.show_community_scripts = request.data["show_community_scripts"]
user.save(update_fields=["show_community_scripts"])
if "userui" in request.data.keys():
user.agent_dblclick_action = request.data["agent_dblclick_action"]
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
return Response("ok")

View File

@@ -26,7 +26,7 @@ def get_wmi_data():
agent = Recipe(
Agent,
hostname="DESKTOP-TEST123",
version="1.1.1",
version="1.3.0",
monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",

View File

@@ -1,4 +1,3 @@
import requests
import time
import base64
from Crypto.Cipher import AES
@@ -9,6 +8,7 @@ import validators
import msgpack
import re
from collections import Counter
from typing import List
from loguru import logger
from packaging import version as pyver
from distutils.version import LooseVersion
@@ -117,14 +117,6 @@ class Agent(BaseAuditModel):
return settings.DL_32
return None
@property
def winsalt_dl(self):
if self.arch == "64":
return settings.SALT_64
elif self.arch == "32":
return settings.SALT_32
return None
@property
def win_inno_exe(self):
if self.arch == "64":
@@ -382,14 +374,15 @@ class Agent(BaseAuditModel):
return patch_policy
# clear is used to delete managed policy checks from agent
# parent_checks specifies a list of checks to delete from agent with matching parent_check field
def generate_checks_from_policies(self, clear=False):
from automation.models import Policy
def get_approved_update_guids(self) -> List[str]:
return list(
self.winupdates.filter(action="approve", installed=False).values_list(
"guid", flat=True
)
)
# Clear agent checks managed by policy
if clear:
self.agentchecks.filter(managed_by_policy=True).delete()
def generate_checks_from_policies(self):
from automation.models import Policy
# Clear agent checks that have overriden_by_policy set
self.agentchecks.update(overriden_by_policy=False)
@@ -397,17 +390,9 @@ class Agent(BaseAuditModel):
# Generate checks based on policies
Policy.generate_policy_checks(self)
# clear is used to delete managed policy tasks from agent
# parent_tasks specifies a list of tasks to delete from agent with matching parent_task field
def generate_tasks_from_policies(self, clear=False):
from autotasks.tasks import delete_win_task_schedule
def generate_tasks_from_policies(self):
from automation.models import Policy
# Clear agent tasks managed by policy
if clear:
for task in self.autotasks.filter(managed_by_policy=True):
delete_win_task_schedule.delay(task.pk)
# Generate tasks based on policies
Policy.generate_policy_tasks(self)
@@ -466,77 +451,6 @@ class Agent(BaseAuditModel):
await nc.flush()
await nc.close()
def salt_api_cmd(self, **kwargs):
# salt should always timeout first before the requests' timeout
try:
timeout = kwargs["timeout"]
except KeyError:
# default timeout
timeout = 15
salt_timeout = 12
else:
if timeout < 8:
timeout = 8
salt_timeout = 5
else:
salt_timeout = timeout - 3
json = {
"client": "local",
"tgt": self.salt_id,
"fun": kwargs["func"],
"timeout": salt_timeout,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[json],
timeout=timeout,
)
except Exception:
return "timeout"
try:
ret = resp.json()["return"][0][self.salt_id]
except Exception as e:
logger.error(f"{self.salt_id}: {e}")
return "error"
else:
return ret
def salt_api_async(self, **kwargs):
json = {
"client": "local_async",
"tgt": self.salt_id,
"fun": kwargs["func"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
except Exception:
return "timeout"
return resp
@staticmethod
def serialize(agent):
# serializes the agent and returns json
@@ -547,32 +461,6 @@ class Agent(BaseAuditModel):
del ret["client"]
return ret
@staticmethod
def salt_batch_async(**kwargs):
assert isinstance(kwargs["minions"], list)
json = {
"client": "local_async",
"tgt_type": "list",
"tgt": kwargs["minions"],
"fun": kwargs["func"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
except Exception:
return "timeout"
return resp
def delete_superseded_updates(self):
try:
pks = [] # list of pks to delete
@@ -625,6 +513,13 @@ class Agent(BaseAuditModel):
elif action.details["action"] == "taskdelete":
delete_win_task_schedule.delay(task_id, pending_action=action.id)
# for clearing duplicate pending actions on agent
def remove_matching_pending_task_actions(self, task_id):
# remove any other pending actions on agent with same task_id
for action in self.pendingactions.exclude(status="completed"):
if action.details["task_id"] == task_id:
action.delete()
class AgentOutage(models.Model):
agent = models.ForeignKey(

View File

@@ -34,6 +34,12 @@ class AgentSerializer(serializers.ModelSerializer):
]
class AgentOverdueActionSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = ["pk", "overdue_email_alert", "overdue_text_alert"]
class AgentTableSerializer(serializers.ModelSerializer):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField()
@@ -42,17 +48,30 @@ class AgentTableSerializer(serializers.ModelSerializer):
last_seen = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name")
logged_username = serializers.SerializerMethodField()
italic = serializers.SerializerMethodField()
def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count()
def get_last_seen(self, obj):
def get_last_seen(self, obj) -> str:
if obj.time_zone is not None:
agent_tz = pytz.timezone(obj.time_zone)
else:
agent_tz = self.context["default_tz"]
return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S")
return obj.last_seen.astimezone(agent_tz).timestamp()
def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == "online":
return obj.last_logged_in_user
elif obj.logged_in_username != "None":
return obj.logged_in_username
else:
return "-"
def get_italic(self, obj) -> bool:
return obj.logged_in_username == "None" and obj.status == "online"
class Meta:
model = Agent
@@ -73,9 +92,9 @@ class AgentTableSerializer(serializers.ModelSerializer):
"last_seen",
"boot_time",
"checks",
"logged_in_username",
"last_logged_in_user",
"maintenance_mode",
"logged_username",
"italic",
]
depth = 2

View File

@@ -2,8 +2,6 @@ import asyncio
from loguru import logger
from time import sleep
import random
import requests
from concurrent.futures import ThreadPoolExecutor
from packaging import version as pyver
from typing import List
@@ -18,40 +16,6 @@ from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG)
def _check_agent_service(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong":
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery"
)
data = {"func": "recover", "payload": {"mode": "tacagent"}}
asyncio.run(agent.nats_cmd(data, wait=False))
def _check_in_full(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False))
@app.task
def check_in_task() -> None:
q = Agent.objects.only("pk", "version")
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) >= pyver.parse("1.1.12")
]
with ThreadPoolExecutor() as executor:
executor.map(_check_in_full, agents)
@app.task
def monitor_agents_task() -> None:
q = Agent.objects.all()
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
with ThreadPoolExecutor() as executor:
executor.map(_check_agent_service, agents)
def agent_update(pk: int) -> str:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
@@ -59,9 +23,18 @@ def agent_update(pk: int) -> str:
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
return "noarch"
version = settings.LATEST_AGENT_VER
url = agent.winagent_dl
inno = agent.win_inno_exe
# removed sqlite in 1.4.0 to get rid of cgo dependency
# 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
if pyver.parse(agent.version) >= pyver.parse("1.3.0"):
version = settings.LATEST_AGENT_VER
url = agent.winagent_dl
inno = agent.win_inno_exe
else:
version = "1.3.0"
inno = (
"winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe"
)
url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
if agent.has_nats:
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
@@ -97,6 +70,10 @@ def agent_update(pk: int) -> str:
asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created"
else:
logger.warning(
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to update."
)
return "not supported"
@@ -107,16 +84,18 @@ def send_agent_update_task(pks: List[int], version: str) -> None:
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
]
for pk in agents:
agent_update(pk)
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
for chunk in chunks:
for pk in chunk:
agent_update(pk)
sleep(0.05)
sleep(4)
@app.task
def auto_self_agent_update_task() -> None:
core = CoreSettings.objects.first()
if not core.agent_auto_update:
logger.info("Agent auto update is disabled. Skipping.")
return
q = Agent.objects.only("pk", "version")
@@ -126,108 +105,12 @@ def auto_self_agent_update_task() -> None:
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
for pk in pks:
agent_update(pk)
@app.task
def sync_sysinfo_task():
agents = Agent.objects.all()
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online"
]
for agent in online:
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
@app.task
def sync_salt_modules_task(pk):
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules")
# successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]}
# successful sync with no new/changed files: {'return': [{'MINION-15': []}]}
if r == "timeout" or r == "error":
return f"Unable to sync modules {agent.salt_id}"
return f"Successfully synced salt modules on {agent.hostname}"
@app.task
def batch_sync_modules_task():
# sync modules, split into chunks of 50 agents to not overload salt
agents = Agent.objects.all()
online = [i.salt_id for i in agents]
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
for chunk in chunks:
Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
sleep(10)
@app.task
def uninstall_agent_task(salt_id, has_nats):
attempts = 0
error = False
if not has_nats:
while 1:
try:
r = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[
{
"client": "local",
"tgt": salt_id,
"fun": "win_agent.uninstall_agent",
"timeout": 8,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=10,
)
ret = r.json()["return"][0][salt_id]
except Exception:
attempts += 1
else:
if ret != "ok":
attempts += 1
else:
attempts = 0
if attempts >= 10:
error = True
break
elif attempts == 0:
break
if error:
logger.error(f"{salt_id} uninstall failed")
else:
logger.info(f"{salt_id} was successfully uninstalled")
try:
r = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[
{
"client": "wheel",
"fun": "key.delete",
"match": salt_id,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=30,
)
except Exception:
logger.error(f"{salt_id} unable to remove salt-key")
return "ok"
for pk in chunk:
agent_update(pk)
sleep(0.05)
sleep(4)
@app.task
@@ -282,6 +165,10 @@ def agent_outages_task():
outage = AgentOutage(agent=agent)
outage.save()
# add a null check history to allow gaps in graph
for check in agent.agentchecks.all():
check.add_check_history(None)
if agent.overdue_email_alert and not agent.maintenance_mode:
agent_outage_email_task.delay(pk=outage.pk)
@@ -290,10 +177,17 @@ def agent_outages_task():
@app.task
def install_salt_task(pk: int) -> None:
sleep(20)
agent = Agent.objects.get(pk=pk)
asyncio.run(agent.nats_cmd({"func": "installsalt"}, wait=False))
def handle_agent_recovery_task(pk: int) -> None:
sleep(10)
from agents.models import RecoveryAction
action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}
asyncio.run(action.agent.nats_cmd(data, wait=False))
@app.task
@@ -343,3 +237,18 @@ def run_script_email_results_task(
server.quit()
except Exception as e:
logger.error(e)
@app.task
def remove_salt_task() -> None:
if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT:
return
q = Agent.objects.only("pk", "version")
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")]
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
for chunk in chunks:
for agent in chunk:
asyncio.run(agent.nats_cmd({"func": "removesalt"}, wait=False))
sleep(0.1)
sleep(4)

View File

@@ -14,12 +14,6 @@ from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .tasks import (
agent_recovery_sms_task,
auto_self_agent_update_task,
sync_salt_modules_task,
batch_sync_modules_task,
)
from winupdate.models import WinUpdatePolicy
@@ -110,9 +104,8 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
@patch("agents.tasks.uninstall_agent_task.delay")
@patch("agents.views.reload_nats")
def test_uninstall(self, reload_nats, mock_task, nats_cmd):
def test_uninstall(self, reload_nats, nats_cmd):
url = "/agents/uninstall/"
data = {"pk": self.agent.pk}
@@ -121,13 +114,18 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.assert_called_with({"func": "uninstall"}, wait=False)
reload_nats.assert_called_once()
mock_task.assert_called_with(self.agent.salt_id, True)
self.check_not_authenticated("delete", url)
@patch("agents.models.Agent.nats_cmd")
def test_get_processes(self, mock_ret):
url = f"/agents/{self.agent.pk}/getprocs/"
agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
url_old = f"/agents/{agent_old.pk}/getprocs/"
r = self.client.get(url_old)
self.assertEqual(r.status_code, 400)
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
url = f"/agents/{agent.pk}/getprocs/"
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/procs.json")
@@ -137,9 +135,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
assert any(i["name"] == "Registry" for i in mock_ret.return_value)
assert any(
i["memory_percent"] == 0.004843281375620747 for i in mock_ret.return_value
)
assert any(i["membytes"] == 434655234324 for i in mock_ret.return_value)
mock_ret.return_value = "timeout"
r = self.client.get(url)
@@ -166,18 +162,44 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
def test_get_event_log(self, mock_ret):
url = f"/agents/{self.agent.pk}/geteventlog/Application/30/"
def test_get_event_log(self, nats_cmd):
url = f"/agents/{self.agent.pk}/geteventlog/Application/22/"
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
) as f:
mock_ret.return_value = json.load(f)
nats_cmd.return_value = json.load(f)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": "Application",
"days": str(22),
},
},
timeout=32,
)
mock_ret.return_value = "timeout"
url = f"/agents/{self.agent.pk}/geteventlog/Security/6/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 180,
"payload": {
"logname": "Security",
"days": str(6),
},
},
timeout=182,
)
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
@@ -331,7 +353,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
data["mode"] = "salt"
data["mode"] = "mesh"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json())
@@ -351,7 +373,7 @@ class TestAgentViews(TacticalTestCase):
self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"])
data["mode"] = "salt"
data["mode"] = "mesh"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json())
@@ -483,42 +505,20 @@ class TestAgentViews(TacticalTestCase):
def test_overdue_action(self):
url = "/agents/overdueaction/"
payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"}
payload = {"pk": self.agent.pk, "overdue_email_alert": True}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "disabled"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "enabled"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "disabled"})
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "523423"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload.update({"alertType": "text", "action": "asdasd3434asdasd"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)
def test_list_agents_no_detail(self):
@@ -539,7 +539,7 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("winupdate.tasks.bulk_check_for_updates_task.delay")
""" @patch("winupdate.tasks.bulk_check_for_updates_task.delay")
@patch("scripts.tasks.handle_bulk_script_task.delay")
@patch("scripts.tasks.handle_bulk_command_task.delay")
@patch("agents.models.Agent.salt_batch_async")
@@ -581,7 +581,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
""" payload = {
payload = {
"mode": "command",
"monType": "workstations",
"target": "client",
@@ -595,7 +595,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300) """
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300)
payload = {
"mode": "command",
@@ -653,7 +653,7 @@ class TestAgentViews(TacticalTestCase):
# TODO mock the script
self.check_not_authenticated("post", url)
self.check_not_authenticated("post", url) """
@patch("agents.models.Agent.nats_cmd")
def test_recover_mesh(self, nats_cmd):
@@ -755,41 +755,6 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_salt_modules_task(self, salt_api_cmd):
self.agent = baker.make_recipe("agents.agent")
salt_api_cmd.return_value = {"return": [{f"{self.agent.salt_id}": []}]}
ret = sync_salt_modules_task.s(self.agent.pk).apply()
salt_api_cmd.assert_called_with(timeout=35, func="saltutil.sync_modules")
self.assertEqual(
ret.result, f"Successfully synced salt modules on {self.agent.hostname}"
)
self.assertEqual(ret.status, "SUCCESS")
salt_api_cmd.return_value = "timeout"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
salt_api_cmd.return_value = "error"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async):
# chunks of 50, should run 4 times
baker.make_recipe(
"agents.online_agent", last_seen=djangotime.now(), _quantity=60
)
baker.make_recipe(
"agents.overdue_agent",
last_seen=djangotime.now() - djangotime.timedelta(minutes=9),
_quantity=115,
)
ret = batch_sync_modules_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 4)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.nats_cmd")
def test_agent_update(self, nats_cmd):
from agents.tasks import agent_update
@@ -819,19 +784,20 @@ class TestAgentTasks(TacticalTestCase):
action = PendingAction.objects.get(agent__pk=agent64_111.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(action.details["url"], settings.DL_64)
self.assertEqual(
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
action.details["url"],
"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
)
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
self.assertEqual(action.details["version"], "1.3.0")
agent64 = baker.make_recipe(
agent_64_130 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.12",
version="1.3.0",
)
nats_cmd.return_value = "ok"
r = agent_update(agent64.pk)
r = agent_update(agent_64_130.pk)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
@@ -845,6 +811,26 @@ class TestAgentTasks(TacticalTestCase):
wait=False,
)
agent64_old = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.2.1",
)
nats_cmd.return_value = "ok"
r = agent_update(agent64_old.pk)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
"version": "1.3.0",
"inno": "winagent-v1.3.0.exe",
},
},
wait=False,
)
""" @patch("agents.models.Agent.salt_api_async")
@patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):

View File

@@ -7,6 +7,7 @@ import random
import string
import datetime as dt
from packaging import version as pyver
from typing import List
from django.conf import settings
from django.shortcuts import get_object_or_404
@@ -29,15 +30,15 @@ from .serializers import (
AgentEditSerializer,
NoteSerializer,
NotesSerializer,
AgentOverdueActionSerializer,
)
from winupdate.serializers import WinUpdatePolicySerializer
from .tasks import (
uninstall_agent_task,
send_agent_update_task,
run_script_email_results_task,
)
from winupdate.tasks import bulk_check_for_updates_task
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import notify_error, reload_nats
@@ -72,10 +73,6 @@ def ping(request, pk):
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
else:
r = agent.salt_api_cmd(timeout=5, func="test.ping")
if isinstance(r, bool) and r:
status = "online"
return Response({"name": agent.hostname, "status": status})
@@ -86,13 +83,9 @@ def uninstall(request):
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
salt_id = agent.salt_id
name = agent.hostname
has_nats = agent.has_nats
agent.delete()
reload_nats()
uninstall_agent_task.delay(salt_id, has_nats)
return Response(f"{name} will now be uninstalled.")
@@ -114,8 +107,8 @@ def edit_agent(request):
# check if site changed and initiate generating correct policies
if old_site != request.data["site"]:
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
return Response("ok")
@@ -159,12 +152,12 @@ def agent_detail(request, pk):
@api_view()
def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if pyver.parse(agent.version) < pyver.parse("1.2.0"):
return notify_error("Requires agent version 1.2.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout":
return notify_error("Unable to contact the agent")
return Response(r)
@@ -191,15 +184,16 @@ def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = 180 if logtype == "Security" else 30
data = {
"func": "eventlog",
"timeout": 30,
"timeout": timeout,
"payload": {
"logname": logtype,
"days": str(days),
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=32))
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout":
return notify_error("Unable to contact the agent")
@@ -341,26 +335,12 @@ def by_site(request, sitepk):
@api_view(["POST"])
def overdue_action(request):
pk = request.data["pk"]
alert_type = request.data["alertType"]
action = request.data["action"]
agent = get_object_or_404(Agent, pk=pk)
if alert_type == "email" and action == "enabled":
agent.overdue_email_alert = True
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "email" and action == "disabled":
agent.overdue_email_alert = False
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "text" and action == "enabled":
agent.overdue_text_alert = True
agent.save(update_fields=["overdue_text_alert"])
elif alert_type == "text" and action == "disabled":
agent.overdue_text_alert = False
agent.save(update_fields=["overdue_text_alert"])
else:
return Response(
{"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
)
agent = get_object_or_404(Agent, pk=request.data["pk"])
serializer = AgentOverdueActionSerializer(
instance=agent, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(agent.hostname)
@@ -481,7 +461,7 @@ def install_agent(request):
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-X 'main.Inno={inno}'",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'",
@@ -611,8 +591,6 @@ def install_agent(request):
resp = {
"cmd": " ".join(str(i) for i in cmd),
"url": download_url,
"salt64": settings.SALT_64,
"salt32": settings.SALT_32,
}
return Response(resp)
@@ -673,17 +651,12 @@ def recover(request):
return notify_error("Only available in agent version greater than 0.9.5")
if not agent.has_nats:
if mode == "tacagent" or mode == "checkrunner" or mode == "rpc":
if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if (
mode == "tacagent"
or mode == "checkrunner"
or mode == "salt"
or mode == "mesh"
):
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
@@ -840,7 +813,7 @@ def bulk(request):
elif request.data["target"] == "agents":
q = Agent.objects.filter(pk__in=request.data["agentPKs"])
elif request.data["target"] == "all":
q = Agent.objects.all()
q = Agent.objects.only("pk", "monitoring_type")
else:
return notify_error("Something went wrong")
@@ -849,8 +822,7 @@ def bulk(request):
elif request.data["monType"] == "workstations":
q = q.filter(monitoring_type="workstation")
minions = [agent.salt_id for agent in q]
agents = [agent.pk for agent in q]
agents: List[int] = [agent.pk for agent in q]
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
@@ -868,14 +840,12 @@ def bulk(request):
return Response(f"{script.name} will now be run on {len(agents)} agents")
elif request.data["mode"] == "install":
r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates")
if r == "timeout":
return notify_error("Salt API not running")
bulk_install_updates_task.delay(agents)
return Response(
f"Pending updates will now be installed on {len(agents)} agents"
)
elif request.data["mode"] == "scan":
bulk_check_for_updates_task.delay(minions=minions)
bulk_check_for_updates_task.delay(agents)
return Response(f"Patch status scan will now run on {len(agents)} agents")
return notify_error("Something went wrong")

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class Apiv2Config(AppConfig):
name = "apiv2"

View File

@@ -1,38 +0,0 @@
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
class TestAPIv2(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_modules(self, mock_ret):
# setup data
agent = baker.make_recipe("agents.agent")
url = "/api/v2/saltminion/"
payload = {"agent_id": agent.agent_id}
mock_ret.return_value = "error"
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 400)
mock_ret.return_value = []
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Modules are already in sync")
mock_ret.return_value = ["modules.win_agent"]
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Successfully synced salt modules")
mock_ret.return_value = ["askdjaskdjasd", "modules.win_agent"]
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Successfully synced salt modules")
self.check_not_authenticated("patch", url)

View File

@@ -1,14 +0,0 @@
from django.urls import path
from . import views
from apiv3 import views as v3_views
urlpatterns = [
path("newagent/", v3_views.NewAgent.as_view()),
path("meshexe/", v3_views.MeshExe.as_view()),
path("saltminion/", v3_views.SaltMinion.as_view()),
path("<str:agentid>/saltminion/", v3_views.SaltMinion.as_view()),
path("sysinfo/", v3_views.SysInfo.as_view()),
path("hello/", v3_views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
]

View File

@@ -1,41 +0,0 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializerV2
class CheckRunner(APIView):
"""
For the windows python agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV2(checks, many=True).data,
}
return Response(ret)
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data)
return Response(status)

View File

@@ -26,23 +26,6 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_get_salt_minion(self):
url = f"/api/v3/{self.agent.agent_id}/saltminion/"
url2 = f"/api/v2/{self.agent.agent_id}/saltminion/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn("latestVer", r.json().keys())
self.assertIn("currentVer", r.json().keys())
self.assertIn("salt_id", r.json().keys())
self.assertIn("downloadURL", r.json().keys())
r2 = self.client.get(url2)
self.assertEqual(r2.status_code, 200)
self.check_not_authenticated("get", url)
self.check_not_authenticated("get", url2)
def test_get_mesh_info(self):
url = f"/api/v3/{self.agent.pk}/meshinfo/"
@@ -93,11 +76,11 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("patch", url)
@patch("agents.tasks.install_salt_task.delay")
def test_install_salt(self, mock_task):
url = f"/api/v3/{self.agent.agent_id}/installsalt/"
def test_checkrunner_interval(self):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(self.agent.pk)
self.check_not_authenticated("get", url)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)

View File

@@ -6,9 +6,8 @@ urlpatterns = [
path("hello/", views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("saltminion/", views.SaltMinion.as_view()),
path("<str:agentid>/saltminion/", views.SaltMinion.as_view()),
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
path("meshexe/", views.MeshExe.as_view()),
path("sysinfo/", views.SysInfo.as_view()),
@@ -17,5 +16,4 @@ urlpatterns = [
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
path("software/", views.Software.as_view()),
path("installer/", views.Installer.as_view()),
path("<str:agentid>/installsalt/", views.InstallSalt.as_view()),
]

View File

@@ -21,7 +21,7 @@ from autotasks.models import AutomatedTask
from accounts.models import User
from winupdate.models import WinUpdatePolicy
from software.models import InstalledSoftware
from checks.serializers import CheckRunnerGetSerializerV3
from checks.serializers import CheckRunnerGetSerializer
from agents.serializers import WinAgentSerializer
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from winupdate.serializers import ApprovedUpdateSerializer
@@ -29,11 +29,7 @@ from winupdate.serializers import ApprovedUpdateSerializer
from agents.tasks import (
agent_recovery_email_task,
agent_recovery_sms_task,
sync_salt_modules_task,
install_salt_task,
)
from winupdate.tasks import check_for_updates_task
from software.tasks import install_chocolatey
from checks.utils import bytes2human
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
@@ -132,15 +128,6 @@ class CheckIn(APIView):
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(last_seen=djangotime.now())
sync_salt_modules_task.delay(agent.pk)
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
)
if not agent.choco_installed:
install_chocolatey.delay(agent.pk, wait=True)
return Response("ok")
@@ -227,15 +214,6 @@ class Hello(APIView):
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(last_seen=djangotime.now())
sync_salt_modules_task.delay(agent.pk)
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
)
if not agent.choco_installed:
install_chocolatey.delay(agent.pk, wait=True)
return Response("ok")
@@ -254,31 +232,28 @@ class CheckRunner(APIView):
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV3(checks, many=True).data,
"checks": CheckRunnerGetSerializer(checks, many=True).data,
}
return Response(ret)
def patch(self, request):
from logs.models import AuditLog
check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data)
# create audit entry
AuditLog.objects.create(
username=check.agent.hostname,
agent=check.agent.hostname,
object_type="agent",
action="check_run",
message=f"{check.readable_desc} was run on {check.agent.hostname}. Status: {status}",
after_value=Check.serialize(check),
)
return Response(status)
class CheckRunnerInterval(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
class TaskRunner(APIView):
"""
For the windows golang agent
@@ -317,77 +292,6 @@ class TaskRunner(APIView):
return Response("ok")
class SaltMinion(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
ret = {
"latestVer": settings.LATEST_SALT_VER,
"currentVer": agent.salt_ver,
"salt_id": agent.salt_id,
"downloadURL": agent.winsalt_dl,
}
return Response(ret)
def post(self, request):
# accept the salt key
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.salt_id != request.data["saltid"]:
return notify_error("Salt keys do not match")
try:
resp = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[
{
"client": "wheel",
"fun": "key.accept",
"match": request.data["saltid"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=30,
)
except Exception:
return notify_error("No communication between agent and salt-api")
try:
data = resp.json()["return"][0]["data"]
minion = data["return"]["minions"][0]
except Exception:
return notify_error("Key error")
if data["success"] and minion == request.data["saltid"]:
return Response("Salt key was accepted")
else:
return notify_error("Not accepted")
def patch(self, request):
# sync modules
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
r = agent.salt_api_cmd(timeout=45, func="saltutil.sync_modules")
if r == "timeout" or r == "error":
return notify_error("Failed to sync salt modules")
if isinstance(r, list) and any("modules" in i for i in r):
return Response("Successfully synced salt modules")
elif isinstance(r, list) and not r:
return Response("Modules are already in sync")
else:
return notify_error(f"Failed to sync salt modules: {str(r)}")
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.salt_ver = request.data["ver"]
agent.save(update_fields=["salt_ver"])
return Response("ok")
class WinUpdater(APIView):
authentication_classes = [TokenAuthentication]
@@ -428,6 +332,7 @@ class WinUpdater(APIView):
update.installed = True
update.save(update_fields=["result", "downloaded", "installed"])
agent.delete_superseded_updates()
return Response("ok")
# agent calls this after it's finished installing all patches
@@ -449,19 +354,11 @@ class WinUpdater(APIView):
if reboot:
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
else:
agent.salt_api_async(
func="system.reboot",
arg=7,
kwargs={"in_seconds": True},
logger.info(
f"{agent.hostname} is rebooting after updates were installed."
)
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
else:
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
agent.delete_superseded_updates()
return Response("ok")
@@ -627,13 +524,3 @@ class Installer(APIView):
)
return Response("ok")
class InstallSalt(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
install_salt_task.delay(agent.pk)
return Response("ok")

View File

@@ -1,6 +1,5 @@
from django.db import models
from agents.models import Agent
from clients.models import Site, Client
from core.models import CoreSettings
from logs.models import BaseAuditModel
@@ -58,6 +57,11 @@ class Policy(BaseAuditModel):
@staticmethod
def cascade_policy_tasks(agent):
from autotasks.tasks import delete_win_task_schedule
from autotasks.models import AutomatedTask
from logs.models import PendingAction
# List of all tasks to be applied
tasks = list()
added_task_pks = list()
@@ -107,6 +111,33 @@ class Policy(BaseAuditModel):
tasks.append(task)
added_task_pks.append(task.pk)
# remove policy tasks from agent not included in policy
for task in agent.autotasks.filter(
parent_task__in=[
taskpk
for taskpk in agent_tasks_parent_pks
if taskpk not in added_task_pks
]
):
delete_win_task_schedule.delay(task.pk)
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
for action in agent.pendingactions.exclude(status="completed"):
task = AutomatedTask.objects.get(pk=action.details["task_id"])
if (
task.parent_task in agent_tasks_parent_pks
and task.parent_task in added_task_pks
):
agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=agent,
action_type="taskaction",
details={"action": "taskcreate", "task_id": task.id},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@staticmethod
@@ -280,6 +311,15 @@ class Policy(BaseAuditModel):
+ eventlog_checks
)
# remove policy checks from agent that fell out of policy scope
agent.agentchecks.filter(
parent_check__in=[
checkpk
for checkpk in agent_checks_parent_pks
if checkpk not in [check.pk for check in final_list]
]
).delete()
return [
check for check in final_list if check.pk not in agent_checks_parent_pks
]

View File

@@ -6,56 +6,46 @@ from tacticalrmm.celery import app
@app.task
def generate_agent_checks_from_policies_task(
###
# copies the policy checks to all affected agents
#
# clear: clears all policy checks first
# create_tasks: also create tasks after checks are generated
###
policypk,
clear=False,
create_tasks=False,
):
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.all()
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server")
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation")
agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else:
agents = policy.related_agents()
for agent in agents:
agent.generate_checks_from_policies(clear=clear)
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies(
clear=clear,
)
agent.generate_tasks_from_policies()
@app.task
def generate_agent_checks_by_location_task(
location, mon_type, clear=False, create_tasks=False
):
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_checks_from_policies(clear=clear)
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies(clear=clear)
agent.generate_tasks_from_policies()
@app.task
def generate_all_agent_checks_task(mon_type, clear=False, create_tasks=False):
def generate_all_agent_checks_task(mon_type, create_tasks=False):
for agent in Agent.objects.filter(monitoring_type=mon_type):
agent.generate_checks_from_policies(clear=clear)
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies(clear=clear)
agent.generate_tasks_from_policies()
@app.task
@@ -93,28 +83,32 @@ def update_policy_check_fields_task(checkpk):
@app.task
def generate_agent_tasks_from_policies_task(policypk, clear=False):
def generate_agent_tasks_from_policies_task(policypk):
policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.all()
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server")
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation")
agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else:
agents = policy.related_agents()
for agent in agents:
agent.generate_tasks_from_policies(clear=clear)
agent.generate_tasks_from_policies()
@app.task
def generate_agent_tasks_by_location_task(location, mon_type, clear=False):
def generate_agent_tasks_by_location_task(location, mon_type):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_tasks_from_policies(clear=clear)
agent.generate_tasks_from_policies()
@app.task

View File

@@ -121,9 +121,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with(
policypk=policy.pk, clear=True, create_tasks=True
)
mock_checks_task.assert_called_with(policypk=policy.pk, create_tasks=True)
self.check_not_authenticated("put", url)
@@ -140,8 +138,8 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with(policypk=policy.pk, clear=True)
mock_tasks_task.assert_called_with(policypk=policy.pk, clear=True)
mock_checks_task.assert_called_with(policypk=policy.pk)
mock_tasks_task.assert_called_with(policypk=policy.pk)
self.check_not_authenticated("delete", url)
@@ -298,7 +296,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -311,7 +308,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -324,7 +320,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -337,7 +332,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -347,7 +341,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called_with(clear=True)
mock_checks_task.assert_called()
mock_checks_task.reset_mock()
# Adding the same relations shouldn't trigger mocks
@@ -396,7 +390,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -409,7 +402,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -422,7 +414,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -435,7 +426,6 @@ class TestPolicyViews(TacticalTestCase):
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
@@ -444,7 +434,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called_with(clear=True)
mock_checks_task.assert_called()
mock_checks_task.reset_mock()
# adding the same relations shouldn't trigger mocks
@@ -753,7 +743,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id, clear=True)
generate_agent_checks_from_policies_task(policy.id)
# make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -832,7 +822,6 @@ class TestPolicyTasks(TacticalTestCase):
generate_agent_checks_by_location_task(
{"site_id": sites[0].id},
"server",
clear=True,
create_tasks=True,
)
@@ -846,7 +835,6 @@ class TestPolicyTasks(TacticalTestCase):
generate_agent_checks_by_location_task(
{"site__client_id": clients[0].id},
"workstation",
clear=True,
create_tasks=True,
)
# workstation_agent should now have policy checks and the other agents should not
@@ -875,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
core.workstation_policy = policy
core.save()
generate_all_agent_checks_task("server", clear=True, create_tasks=True)
generate_all_agent_checks_task("server", create_tasks=True)
# all servers should have 7 checks
for agent in server_agents:
@@ -884,7 +872,7 @@ class TestPolicyTasks(TacticalTestCase):
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
generate_all_agent_checks_task("workstation", clear=True, create_tasks=True)
generate_all_agent_checks_task("workstation", create_tasks=True)
# all agents should have 7 checks now
for agent in server_agents:
@@ -961,7 +949,7 @@ class TestPolicyTasks(TacticalTestCase):
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
generate_agent_tasks_from_policies_task(policy.id, clear=True)
generate_agent_tasks_from_policies_task(policy.id)
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -1000,9 +988,7 @@ class TestPolicyTasks(TacticalTestCase):
agent1 = baker.make_recipe("agents.agent", site=sites[1])
agent2 = baker.make_recipe("agents.agent", site=sites[3])
generate_agent_tasks_by_location_task(
{"site_id": sites[0].id}, "server", clear=True
)
generate_agent_tasks_by_location_task({"site_id": sites[0].id}, "server")
# all servers in site1 and site2 should have 3 tasks
self.assertEqual(
@@ -1013,7 +999,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
generate_agent_tasks_by_location_task(
{"site__client_id": clients[0].id}, "workstation", clear=True
{"site__client_id": clients[0].id}, "workstation"
)
# all workstations in Default1 should have 3 tasks

View File

@@ -83,7 +83,6 @@ class GetUpdateDeletePolicy(APIView):
if saved_policy.active != old_active or saved_policy.enforced != old_enforced:
generate_agent_checks_from_policies_task.delay(
policypk=policy.pk,
clear=(not saved_policy.active or not saved_policy.enforced),
create_tasks=(saved_policy.active != old_active),
)
@@ -93,8 +92,8 @@ class GetUpdateDeletePolicy(APIView):
policy = get_object_or_404(Policy, pk=pk)
# delete all managed policy checks off of agents
generate_agent_checks_from_policies_task.delay(policypk=policy.pk, clear=True)
generate_agent_tasks_from_policies_task.delay(policypk=policy.pk, clear=True)
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
generate_agent_tasks_from_policies_task.delay(policypk=policy.pk)
policy.delete()
return Response("ok")
@@ -218,7 +217,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
@@ -236,7 +234,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
@@ -258,7 +255,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
@@ -276,7 +272,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
@@ -296,7 +291,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
@@ -311,7 +305,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
@@ -329,7 +322,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
@@ -343,7 +335,6 @@ class GetRelated(APIView):
generate_agent_checks_by_location_task.delay(
location={"site_id": site.pk},
mon_type="server",
clear=True,
create_tasks=True,
)
@@ -358,14 +349,14 @@ class GetRelated(APIView):
if not agent.policy or agent.policy and agent.policy.pk != policy.pk:
agent.policy = policy
agent.save()
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
else:
if agent.policy:
agent.policy = None
agent.save()
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
return Response("ok")
@@ -422,11 +413,15 @@ class UpdatePatchPolicy(APIView):
agents = None
if "client" in request.data:
agents = Agent.objects.filter(site__client_id=request.data["client"])
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site__client_id=request.data["client"]
)
elif "site" in request.data:
agents = Agent.objects.filter(site_id=request.data["site"])
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site_id=request.data["site"]
)
else:
agents = Agent.objects.all()
agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
for agent in agents:
winupdatepolicy = agent.winupdatepolicy.get()

View File

@@ -7,7 +7,7 @@ class Command(BaseCommand):
help = "Checks for orphaned tasks on all agents and removes them"
def handle(self, *args, **kwargs):
agents = Agent.objects.all()
agents = Agent.objects.only("pk", "last_seen", "overdue_time")
online = [i for i in agents if i.status == "online"]
for agent in online:
remove_orphaned_win_tasks.delay(agent.pk)

View File

@@ -6,7 +6,6 @@ import datetime as dt
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.db.models.fields import DateTimeField
from automation.models import Policy
from logs.models import BaseAuditModel
from tacticalrmm.utils import bitdays_to_string
@@ -43,7 +42,7 @@ class AutomatedTask(BaseAuditModel):
blank=True,
)
policy = models.ForeignKey(
Policy,
"automation.Policy",
related_name="autotasks",
null=True,
blank=True,

View File

@@ -76,9 +76,14 @@ def create_win_task_schedule(pk, pending_action=False):
return "error"
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
@@ -144,6 +149,7 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
task.sync_status = "synced"
task.save(update_fields=["sync_status"])
return "ok"
@@ -157,9 +163,13 @@ def delete_win_task_schedule(pk, pending_action=False):
}
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
if r != "ok" and "The system cannot find the file specified" not in r:
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
@@ -168,7 +178,7 @@ def delete_win_task_schedule(pk, pending_action=False):
task.sync_status = "pendingdeletion"
task.save(update_fields=["sync_status"])
return
return "timeout"
# complete pending action since it was successful
if pending_action:
@@ -176,6 +186,9 @@ def delete_win_task_schedule(pk, pending_action=False):
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
task.delete()
return "ok"

View File

@@ -1,5 +1,6 @@
from django.contrib import admin
from .models import Check
from .models import Check, CheckHistory
admin.site.register(Check)
admin.site.register(CheckHistory)

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.4 on 2021-01-09 02:56
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0010_auto_20200922_1344"),
]
operations = [
migrations.AddField(
model_name="check",
name="run_history",
field=django.contrib.postgres.fields.ArrayField(
base_field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
size=None,
),
blank=True,
default=list,
null=True,
size=None,
),
),
]

View File

@@ -0,0 +1,39 @@
# Generated by Django 3.1.4 on 2021-01-09 21:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("checks", "0010_auto_20200922_1344"),
]
operations = [
migrations.CreateModel(
name="CheckHistory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("x", models.DateTimeField()),
("y", models.PositiveIntegerField()),
("results", models.JSONField(blank=True, null=True)),
(
"check_history",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="check_history",
to="checks.check",
),
),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 05:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0011_checkhistory"),
]
operations = [
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 05:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0012_auto_20210110_0503"),
]
operations = [
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(null=True),
),
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("checks", "0013_auto_20210110_0505"),
("checks", "0011_check_run_history"),
]
operations = []

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0014_merge_20210110_1808"),
]
operations = [
migrations.RemoveField(
model_name="check",
name="run_history",
),
migrations.AlterField(
model_name="checkhistory",
name="x",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(blank=True, default=None, null=True),
),
]

View File

@@ -3,12 +3,13 @@ import string
import os
import json
import pytz
from statistics import mean
from statistics import mean, mode
from django.db import models
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework.fields import JSONField
from core.models import CoreSettings
from logs.models import BaseAuditModel
@@ -214,6 +215,9 @@ class Check(BaseAuditModel):
"modified_time",
]
def add_check_history(self, value, more_info=None):
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
def handle_checkv2(self, data):
# cpuload or mem checks
if self.check_type == "cpuload" or self.check_type == "memory":
@@ -232,6 +236,9 @@ class Check(BaseAuditModel):
else:
self.status = "passing"
# add check history
self.add_check_history(data["percent"])
# diskspace checks
elif self.check_type == "diskspace":
if data["exists"]:
@@ -245,6 +252,9 @@ class Check(BaseAuditModel):
self.status = "passing"
self.more_info = f"Total: {total}B, Free: {free}B"
# add check history
self.add_check_history(percent_used)
else:
self.status = "failing"
self.more_info = f"Disk {self.disk} does not exist"
@@ -277,6 +287,17 @@ class Check(BaseAuditModel):
]
)
# add check history
self.add_check_history(
1 if self.status == "failing" else 0,
{
"retcode": data["retcode"],
"stdout": data["stdout"][:60],
"stderr": data["stderr"][:60],
"execution_time": self.execution_time,
},
)
# ping checks
elif self.check_type == "ping":
success = ["Reply", "bytes", "time", "TTL"]
@@ -293,6 +314,10 @@ class Check(BaseAuditModel):
self.more_info = output
self.save(update_fields=["more_info"])
self.add_check_history(
1 if self.status == "failing" else 0, self.more_info[:60]
)
# windows service checks
elif self.check_type == "winsvc":
svc_stat = data["status"]
@@ -332,6 +357,10 @@ class Check(BaseAuditModel):
self.save(update_fields=["more_info"])
self.add_check_history(
1 if self.status == "failing" else 0, self.more_info[:60]
)
elif self.check_type == "eventlog":
log = []
is_wildcard = self.event_id_is_wildcard
@@ -391,6 +420,11 @@ class Check(BaseAuditModel):
self.extra_details = {"log": log}
self.save(update_fields=["extra_details"])
self.add_check_history(
1 if self.status == "failing" else 0,
"Events Found:" + str(len(self.extra_details["log"])),
)
# handle status
if self.status == "failing":
self.fail_count += 1
@@ -411,42 +445,6 @@ class Check(BaseAuditModel):
return self.status
def handle_check(self, data):
if self.check_type != "cpuload" and self.check_type != "memory":
if data["status"] == "passing" and self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["fail_count"])
elif data["status"] == "failing":
self.fail_count += 1
self.save(update_fields=["fail_count"])
else:
self.history.append(data["percent"])
if len(self.history) > 15:
self.history = self.history[-15:]
self.save(update_fields=["history"])
avg = int(mean(self.history))
if avg > self.threshold:
self.status = "failing"
self.fail_count += 1
self.save(update_fields=["status", "fail_count"])
else:
self.status = "passing"
if self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["status", "fail_count"])
else:
self.save(update_fields=["status"])
if self.email_alert and self.fail_count >= self.fails_b4_alert:
handle_check_email_alert_task.delay(self.pk)
@staticmethod
def serialize(check):
# serializes the check and returns json
@@ -645,3 +643,17 @@ class Check(BaseAuditModel):
body = subject
CORE.send_sms(body)
class CheckHistory(models.Model):
check_history = models.ForeignKey(
Check,
related_name="check_history",
on_delete=models.CASCADE,
)
x = models.DateTimeField(auto_now_add=True)
y = models.PositiveIntegerField(null=True, blank=True, default=None)
results = models.JSONField(null=True, blank=True)
def __str__(self):
return self.check_history.readable_desc

View File

@@ -1,8 +1,8 @@
import validators as _v
import pytz
from rest_framework import serializers
from .models import Check
from .models import Check, CheckHistory
from autotasks.models import AutomatedTask
from scripts.serializers import ScriptSerializer, ScriptCheckSerializer
@@ -95,101 +95,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer):
# for the windows agent
# only send data needed for agent to run a check
assigned_task = serializers.SerializerMethodField()
script = ScriptSerializer(read_only=True)
def get_assigned_task(self, obj):
if obj.assignedtask.exists():
# this will not break agents on version 0.10.2 or lower
# newer agents once released will properly handle multiple tasks assigned to a check
task = obj.assignedtask.first()
return AssignedTaskCheckRunnerField(task).data
class Meta:
model = Check
exclude = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV2(serializers.ModelSerializer):
# for the windows __python__ agent
# only send data needed for agent to run a check
assigned_tasks = serializers.SerializerMethodField()
script = ScriptSerializer(read_only=True)
def get_assigned_tasks(self, obj):
if obj.assignedtask.exists():
tasks = obj.assignedtask.all()
return AssignedTaskCheckRunnerField(tasks, many=True).data
class Meta:
model = Check
exclude = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
# for the windows __golang__ agent
# only send data needed for agent to run a check
# the difference here is in the script serializer
# script checks no longer rely on salt and are executed directly by the go agent
assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True)
@@ -237,3 +143,15 @@ class CheckResultsSerializer(serializers.ModelSerializer):
class Meta:
model = Check
fields = "__all__"
class CheckHistorySerializer(serializers.ModelSerializer):
x = serializers.SerializerMethodField()
def get_x(self, obj):
return obj.x.astimezone(pytz.timezone(self.context["timezone"])).isoformat()
# used for return large amounts of graph data
class Meta:
model = CheckHistory
fields = ("x", "y", "results")

View File

@@ -5,8 +5,6 @@ from time import sleep
from tacticalrmm.celery import app
from django.utils import timezone as djangotime
from agents.models import Agent
@app.task
def handle_check_email_alert_task(pk):
@@ -56,3 +54,15 @@ def handle_check_sms_alert_task(pk):
check.save(update_fields=["text_sent"])
return "ok"
@app.task
def prune_check_history(older_than_days: int) -> str:
from .models import CheckHistory
CheckHistory.objects.filter(
x__lt=djangotime.make_aware(dt.datetime.today())
- djangotime.timedelta(days=older_than_days)
).delete()
return "ok"

View File

@@ -1,13 +1,16 @@
from checks.models import CheckHistory
from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer
from django.utils import timezone as djangotime
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
class TestCheckViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_disk_check(self):
# setup data
@@ -180,3 +183,111 @@ class TestCheckViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("patch", url_a)
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_old.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
nats_cmd.reset_mock()
nats_cmd.return_value = "busy"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}")
nats_cmd.reset_mock()
nats_cmd.return_value = "ok"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(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"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), "Unable to contact the agent")
self.check_not_authenticated("get", url)
def test_get_check_history(self):
# setup data
agent = baker.make_recipe("agents.agent")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
_quantity=30,
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
check_history.save()
# test invalid check pk
resp = self.client.patch("/checks/history/500/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/history/{check.id}/"
# test with timeFilter last 30 days
data = {"timeFilter": 30}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 30)
# test with timeFilter equal to 0
data = {"timeFilter": 0}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 60)
self.check_not_authenticated("patch", url)
class TestCheckTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_prune_check_history(self):
from .tasks import prune_check_history
# setup data
check = baker.make_recipe("checks.diskspace_check")
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
_quantity=30,
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
check_history.save()
# prune data 30 days old
prune_check_history(30)
self.assertEqual(CheckHistory.objects.count(), 30)
# prune all Check history Data
prune_check_history(0)
self.assertEqual(CheckHistory.objects.count(), 0)

View File

@@ -7,4 +7,5 @@ urlpatterns = [
path("<pk>/loadchecks/", views.load_checks),
path("getalldisks/", views.get_disks_for_policies),
path("runchecks/<pk>/", views.run_checks),
path("history/<int:checkpk>/", views.CheckHistory.as_view()),
]

View File

@@ -1,6 +1,11 @@
import asyncio
from packaging import version as pyver
from django.shortcuts import get_object_or_404
from django.db.models import Q
from django.utils import timezone as djangotime
from datetime import datetime as dt
from rest_framework.views import APIView
from rest_framework.response import Response
@@ -13,7 +18,7 @@ from automation.models import Policy
from .models import Check
from scripts.models import Script
from .serializers import CheckSerializer
from .serializers import CheckSerializer, CheckHistorySerializer
from automation.tasks import (
@@ -135,14 +140,46 @@ class GetUpdateDeleteCheck(APIView):
return Response(f"{check.readable_desc} was deleted!")
class CheckHistory(APIView):
def patch(self, request, checkpk):
check = get_object_or_404(Check, pk=checkpk)
timeFilter = Q()
if "timeFilter" in request.data:
if request.data["timeFilter"] != 0:
timeFilter = Q(
x__lte=djangotime.make_aware(dt.today()),
x__gt=djangotime.make_aware(dt.today())
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = check.check_history.filter(timeFilter).order_by("-x")
return Response(
CheckHistorySerializer(
check_history, context={"timezone": check.agent.timezone}, many=True
).data
)
@api_view()
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(agent.hostname)
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
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")
else:
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(f"Checks will now be re-run on {agent.hostname}")
@api_view()

View File

@@ -192,7 +192,7 @@ class GenerateAgent(APIView):
if not os.path.exists(go_bin):
return notify_error("Missing golang")
api = f"{request.scheme}://{request.get_host()}"
api = f"https://{request.get_host()}"
inno = (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
if d.arch == "64"
@@ -223,7 +223,7 @@ class GenerateAgent(APIView):
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-X 'main.Inno={inno}'",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={d.client.pk}'",
f"-X 'main.Site={d.site.pk}'",

View File

@@ -57,7 +57,6 @@ func main() {
debugLog := flag.String("log", "", "Verbose output")
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
noSalt := flag.Bool("nosalt", false, "Does not install salt")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
@@ -86,10 +85,6 @@ func main() {
cmdArgs = append(cmdArgs, "-silent")
}
if *noSalt {
cmdArgs = append(cmdArgs, "-nosalt")
}
if len(strings.TrimSpace(*localMesh)) != 0 {
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
}

View File

@@ -16,7 +16,7 @@ class Command(BaseCommand):
# 10-16-2020 changed the type of the agent's 'disks' model field
# from a dict of dicts, to a list of disks in the golang agent
# the following will convert dicts to lists for agent's still on the python agent
agents = Agent.objects.all()
agents = Agent.objects.only("pk", "disks")
for agent in agents:
if agent.disks is not None and isinstance(agent.disks, dict):
new = []

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0011_auto_20201026_0719"),
]
operations = [
migrations.AddField(
model_name="coresettings",
name="check_history_prune_days",
field=models.PositiveIntegerField(default=30),
),
]

View File

@@ -49,6 +49,8 @@ class CoreSettings(BaseAuditModel):
default_time_zone = models.CharField(
max_length=255, choices=TZ_CHOICES, default="America/Los_Angeles"
)
# removes check history older than days
check_history_prune_days = models.PositiveIntegerField(default=30)
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")

View File

@@ -4,8 +4,10 @@ from loguru import logger
from django.conf import settings
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
from core.models import CoreSettings
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
from checks.tasks import prune_check_history
logger.configure(**settings.LOG_CONFIG)
@@ -25,3 +27,7 @@ def core_maintenance_tasks():
if now > task_time_utc:
delete_win_task_schedule.delay(task.pk)
# remove old CheckHistory data
older_than = CoreSettings.objects.first().check_history_prune_days
prune_check_history.delay(older_than)

View File

@@ -83,8 +83,9 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("patch", url)
@patch("tacticalrmm.utils.reload_nats")
@patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
def test_ui_maintenance_actions(self, remove_orphaned_win_tasks):
def test_ui_maintenance_actions(self, remove_orphaned_win_tasks, reload_nats):
url = "/core/servermaintenance/"
agents = baker.make_recipe("agents.online_agent", _quantity=3)
@@ -103,6 +104,7 @@ class TestCoreTasks(TacticalTestCase):
data = {"action": "reload_nats"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
reload_nats.assert_called_once()
# test prune db with no tables
data = {"action": "prune_db"}

View File

@@ -51,14 +51,10 @@ def edit_settings(request):
# check if default policies changed
if old_server_policy != new_settings.server_policy:
generate_all_agent_checks_task.delay(
mon_type="server", clear=True, create_tasks=True
)
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
if old_workstation_policy != new_settings.workstation_policy:
generate_all_agent_checks_task.delay(
mon_type="workstation", clear=True, create_tasks=True
)
generate_all_agent_checks_task.delay(mon_type="workstation", create_tasks=True)
return Response("ok")
@@ -75,6 +71,8 @@ def dashboard_info(request):
"trmm_version": settings.TRMM_VERSION,
"dark_mode": request.user.dark_mode,
"show_community_scripts": request.user.show_community_scripts,
"dbl_click_action": request.user.agent_dblclick_action,
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
}
)
@@ -107,7 +105,7 @@ def server_maintenance(request):
from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks
agents = Agent.objects.all()
agents = Agent.objects.only("pk", "last_seen", "overdue_time")
online = [i for i in agents if i.status == "online"]
for agent in online:
remove_orphaned_win_tasks.delay(agent.pk)

View File

@@ -140,7 +140,7 @@ def cancel_pending_action(request):
def debug_log(request, mode, hostname, order):
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
agents = Agent.objects.all()
agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
agent_hostnames = AgentHostnameSerializer(agents, many=True)
switch_mode = {

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NatsapiConfig(AppConfig):
name = "natsapi"

View File

@@ -0,0 +1,23 @@
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
from django.conf import settings
class TestNatsAPIViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_nats_wmi(self):
url = "/natsapi/wmi/"
baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
)
baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
)
baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.json()["agent_ids"]), 17)

View File

@@ -0,0 +1,13 @@
from django.urls import path
from . import views
urlpatterns = [
path("natsinfo/", views.nats_info),
path("checkin/", views.NatsCheckIn.as_view()),
path("syncmesh/", views.SyncMeshNodeID.as_view()),
path("winupdates/", views.NatsWinUpdates.as_view()),
path("choco/", views.NatsChoco.as_view()),
path("wmi/", views.NatsWMI.as_view()),
path("offline/", views.OfflineAgents.as_view()),
path("logcrash/", views.LogCrash.as_view()),
]

View File

@@ -0,0 +1,286 @@
import asyncio
import time
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from typing import List
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import (
api_view,
permission_classes,
authentication_classes,
)
from django.conf import settings
from django.shortcuts import get_object_or_404
from agents.models import Agent
from winupdate.models import WinUpdate
from software.models import InstalledSoftware
from checks.utils import bytes2human
from agents.serializers import WinAgentSerializer
from agents.tasks import (
agent_recovery_email_task,
agent_recovery_sms_task,
handle_agent_recovery_task,
)
from tacticalrmm.utils import notify_error, filter_software, SoftwareList
logger.configure(**settings.LOG_CONFIG)
@api_view()
@permission_classes([])
@authentication_classes([])
def nats_info(request):
return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY})
class NatsCheckIn(APIView):
authentication_classes = []
permission_classes = []
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.version = request.data["version"]
agent.last_seen = djangotime.now()
agent.save(update_fields=["version", "last_seen"])
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
last_outage = agent.agentoutages.last()
last_outage.recovery_time = djangotime.now()
last_outage.save(update_fields=["recovery_time"])
if agent.overdue_email_alert:
agent_recovery_email_task.delay(pk=last_outage.pk)
if agent.overdue_text_alert:
agent_recovery_sms_task.delay(pk=last_outage.pk)
recovery = agent.recoveryactions.filter(last_run=None).last()
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
handle_agent_recovery_task.delay(pk=recovery.pk)
return Response("ok")
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
agent.handle_pending_actions()
return Response("ok")
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
if request.data["func"] == "disks":
disks = request.data["disks"]
new = []
for disk in disks:
tmp = {}
for _, _ in disk.items():
tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"])
tmp["used"] = bytes2human(disk["used"])
tmp["free"] = bytes2human(disk["free"])
tmp["percent"] = int(disk["percent"])
new.append(tmp)
serializer.is_valid(raise_exception=True)
serializer.save(disks=new)
return Response("ok")
if request.data["func"] == "loggedonuser":
if request.data["logged_in_username"] != "None":
serializer.is_valid(raise_exception=True)
serializer.save(last_logged_in_user=request.data["logged_in_username"])
return Response("ok")
if request.data["func"] == "software":
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return Response("ok")
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
# called once during tacticalagent windows service startup
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if not agent.choco_installed:
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
time.sleep(0.5)
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok")
class SyncMeshNodeID(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.mesh_node_id != request.data["nodeid"]:
agent.mesh_node_id = request.data["nodeid"]
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class NatsChoco(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.choco_installed = request.data["installed"]
agent.save(update_fields=["choco_installed"])
return Response("ok")
class NatsWinUpdates(APIView):
authentication_classes = []
permission_classes = []
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy: str = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["needs_reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
agent.delete_superseded_updates()
return Response("ok")
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
u = agent.winupdates.filter(guid=request.data["guid"]).last()
success: bool = request.data["success"]
if success:
u.result = "success"
u.downloaded = True
u.installed = True
u.date_installed = djangotime.now()
u.save(
update_fields=[
"result",
"downloaded",
"installed",
"date_installed",
]
)
else:
u.result = "failed"
u.save(update_fields=["result"])
agent.delete_superseded_updates()
return Response("ok")
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"]
for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists():
u = agent.winupdates.filter(guid=update["guid"]).last()
u.downloaded = update["downloaded"]
u.installed = update["installed"]
u.save(update_fields=["downloaded", "installed"])
else:
try:
kb = "KB" + update["kb_article_ids"][0]
except:
continue
WinUpdate(
agent=agent,
guid=update["guid"],
kb=kb,
title=update["title"],
installed=update["installed"],
downloaded=update["downloaded"],
description=update["description"],
severity=update["severity"],
categories=update["categories"],
category_ids=update["category_ids"],
kb_article_ids=update["kb_article_ids"],
more_info_urls=update["more_info_urls"],
support_url=update["support_url"],
revision_number=update["revision_number"],
).save()
agent.delete_superseded_updates()
return Response("ok")
class NatsWMI(APIView):
authentication_classes = []
permission_classes = []
def get(self, request):
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time"
)
online: List[str] = [
i.agent_id
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
]
return Response({"agent_ids": online})
class OfflineAgents(APIView):
authentication_classes = []
permission_classes = []
def get(self, request):
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time"
)
offline: List[str] = [
i.agent_id for i in agents if i.has_nats and i.status != "online"
]
return Response({"agent_ids": offline})
class LogCrash(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
)
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
return Response("ok")

View File

@@ -1,3 +1,6 @@
black
Werkzeug
django-extensions
mkdocs
mkdocs-material
pymdown-extensions

View File

@@ -1,38 +1,38 @@
amqp==2.6.1
amqp==5.0.5
asgiref==3.3.1
asyncio-nats-client==0.11.4
billiard==3.6.3.0
celery==4.4.6
celery==5.0.5
certifi==2020.12.5
cffi==1.14.4
chardet==4.0.0
cryptography==3.3.1
decorator==4.4.2
Django==3.1.4
django-cors-headers==3.6.0
Django==3.1.5
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.2
future==0.18.2
idna==2.10
kombu==4.6.11
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.2
packaging==20.8
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.9.9
pyotp==2.4.1
pyotp==2.5.0
pyparsing==2.4.7
pytz==2020.4
pytz==2020.5
qrcode==6.1
redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.50.1
urllib3==1.26.2
twilio==6.51.1
urllib3==1.26.3
uWSGI==2.0.19.1
validators==0.18.2
vine==1.3.0
vine==5.0.0
websockets==8.1
zipp==3.4.0

View File

@@ -110,5 +110,89 @@
"name": "Set High Perf Power Profile",
"description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.",
"shell": "powershell"
},
{
"filename": "Windows10Upgrade.ps1",
"submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm",
"name": "Windows 10 Upgrade",
"description": "Forces an upgrade to the latest release of Windows 10.",
"shell": "powershell"
},
{
"filename": "DiskStatus.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Check Disks",
"description": "Checks local disks for errors reported in event viewer within the last 24 hours",
"shell": "powershell"
},
{
"filename": "DuplicatiStatus.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Check Duplicati",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"shell": "powershell"
},
{
"filename": "EnableDefender.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Enable Windows Defender",
"description": "Enables Windows Defender and sets preferences",
"shell": "powershell"
},
{
"filename": "OpenSSHServerInstall.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Install SSH",
"description": "Installs and enabled OpenSSH Server",
"shell": "powershell"
},
{
"filename": "RDP_enable.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "Enable RDP",
"description": "Enables RDP",
"shell": "cmd"
},
{
"filename": "Speedtest.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "PS Speed Test",
"description": "Powershell speed test (win 10 or server2016+)",
"shell": "powershell"
},
{
"filename": "SyncTime.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "Sync DC Time",
"description": "Syncs time with domain controller",
"shell": "cmd"
},
{
"filename": "WinDefenderClearLogs.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Clear Defender Logs",
"description": "Clears Windows Defender Logs",
"shell": "powershell"
},
{
"filename": "WinDefenderStatus.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Defender Status",
"description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours",
"shell": "powershell"
},
{
"filename": "disable_FastStartup.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "Disable Fast Startup",
"description": "Disables Faststartup on Windows 10",
"shell": "cmd"
},
{
"filename": "updatetacticalexclusion.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "TRMM Defender Exclusions",
"description": "Windows Defender Exclusions for Tactical RMM",
"shell": "powershell"
}
]

View File

@@ -49,7 +49,6 @@ class Script(BaseAuditModel):
# load community uploaded scripts into the database
# skip ones that already exist, only updating name / desc in case it changes
# files will be copied by the update script or in docker to /srv/salt/scripts
# for install script
if not settings.DOCKER_BUILD:
@@ -73,6 +72,7 @@ class Script(BaseAuditModel):
i.name = script["name"]
i.description = script["description"]
i.category = "Community"
i.shell = script["shell"]
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
script_bytes = (
@@ -81,7 +81,13 @@ class Script(BaseAuditModel):
i.code_base64 = base64.b64encode(script_bytes).decode("ascii")
i.save(
update_fields=["name", "description", "category", "code_base64"]
update_fields=[
"name",
"description",
"category",
"code_base64",
"shell",
]
)
else:
print(f"Adding new community script: {script['name']}")

View File

@@ -6,60 +6,26 @@ from scripts.models import Script
@app.task
def handle_bulk_command_task(agentpks, cmd, shell, timeout):
def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
agents_salt = [agent for agent in agents if not agent.has_nats]
minions = [agent.salt_id for agent in agents_salt]
if minions:
Agent.salt_batch_async(
minions=minions,
func="cmd.run_bg",
kwargs={
"cmd": cmd,
"shell": shell,
"timeout": timeout,
},
)
if agents_nats:
nats_data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": cmd,
"shell": shell,
},
}
for agent in agents_nats:
asyncio.run(agent.nats_cmd(nats_data, wait=False))
nats_data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": cmd,
"shell": shell,
},
}
for agent in agents_nats:
asyncio.run(agent.nats_cmd(nats_data, wait=False))
@app.task
def handle_bulk_script_task(scriptpk, agentpks, args, timeout):
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
script = Script.objects.get(pk=scriptpk)
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
agents_salt = [agent for agent in agents if not agent.has_nats]
minions = [agent.salt_id for agent in agents_salt]
if minions:
Agent.salt_batch_async(
minions=minions,
func="win_agent.run_script",
kwargs={
"filepath": script.filepath,
"filename": script.filename,
"shell": script.shell,
"timeout": timeout,
"args": args,
"bg": True if script.shell == "python" else False, # salt bg script bug
},
)
nats_data = {
"func": "runscript",
"timeout": timeout,

View File

@@ -195,15 +195,21 @@ class TestScriptViews(TacticalTestCase):
info = json.load(f)
for script in info:
self.assertTrue(
os.path.exists(os.path.join(scripts_dir, script["filename"]))
)
fn: str = script["filename"]
self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn)))
self.assertTrue(script["filename"])
self.assertTrue(script["name"])
self.assertTrue(script["description"])
self.assertTrue(script["shell"])
self.assertIn(script["shell"], valid_shells)
if fn.endswith(".ps1"):
self.assertEqual(script["shell"], "powershell")
elif fn.endswith(".bat"):
self.assertEqual(script["shell"], "cmd")
elif fn.endswith(".py"):
self.assertEqual(script["shell"], "python")
def test_load_community_scripts(self):
with open(
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")

View File

@@ -1343,10 +1343,5 @@
"name": "tacticalagent",
"description": "Tactical RMM Monitoring Agent",
"display_name": "Tactical RMM Agent"
},
{
"name": "checkrunner",
"description": "Tactical Agent Background Check Runner",
"display_name": "Tactical Agent Check Runner"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -13,5 +13,8 @@ class Command(BaseCommand):
with open(os.path.join(settings.BASE_DIR, "software/chocos.json")) as f:
chocos = json.load(f)
if ChocoSoftware.objects.exists():
ChocoSoftware.objects.all().delete()
ChocoSoftware(chocos=chocos).save()
self.stdout.write("Chocos saved to db")

View File

@@ -7,30 +7,6 @@ class ChocoSoftware(models.Model):
chocos = models.JSONField()
added = models.DateTimeField(auto_now_add=True)
@classmethod
def sort_by_highest(cls):
from .serializers import ChocoSoftwareSerializer
chocos = cls.objects.all()
sizes = [
{"size": len(ChocoSoftwareSerializer(i).data["chocos"]), "pk": i.pk}
for i in chocos
]
biggest = max(range(len(sizes)), key=lambda index: sizes[index]["size"])
return int(sizes[biggest]["pk"])
@classmethod
def combine_all(cls):
from .serializers import ChocoSoftwareSerializer
chocos = cls.objects.all()
combined = []
for i in chocos:
combined.extend(ChocoSoftwareSerializer(i).data["chocos"])
# remove duplicates
return [dict(t) for t in {tuple(d.items()) for d in combined}]
def __str__(self):
from .serializers import ChocoSoftwareSerializer

View File

@@ -1,103 +1,24 @@
import asyncio
from time import sleep
from loguru import logger
from tacticalrmm.celery import app
from django.conf import settings
from django.utils import timezone as djangotime
from agents.models import Agent
from .models import ChocoSoftware, ChocoLog, InstalledSoftware
from tacticalrmm.utils import filter_software
from .models import ChocoLog
logger.configure(**settings.LOG_CONFIG)
@app.task()
def install_chocolatey(pk, wait=False):
if wait:
sleep(15)
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_cmd(timeout=120, func="chocolatey.bootstrap", arg="force=True")
if r == "timeout" or r == "error":
logger.error(f"failed to install choco on {agent.salt_id}")
return
try:
output = r.lower()
except Exception as e:
logger.error(f"failed to install choco on {agent.salt_id}: {e}")
return
success = ["chocolatey", "is", "now", "ready"]
if all(x in output for x in success):
agent.choco_installed = True
agent.save(update_fields=["choco_installed"])
logger.info(f"Installed chocolatey on {agent.salt_id}")
return "ok"
else:
logger.error(f"failed to install choco on {agent.salt_id}")
return
@app.task
def update_chocos():
# delete choco software older than 10 days
try:
first = ChocoSoftware.objects.first().pk
q = ChocoSoftware.objects.exclude(pk=first).filter(
added__lte=djangotime.now() - djangotime.timedelta(days=10)
)
q.delete()
except:
pass
agents = Agent.objects.only("pk")
online = [x for x in agents if x.status == "online" and x.choco_installed]
while 1:
for agent in online:
r = agent.salt_api_cmd(timeout=10, func="test.ping")
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
continue
if isinstance(r, bool) and r:
ret = agent.salt_api_cmd(timeout=200, func="chocolatey.list")
if ret == "timeout" or ret == "error":
continue
try:
chocos = [{"name": k, "version": v[0]} for k, v in ret.items()]
except AttributeError:
continue
else:
# somtimes chocolatey api is down or buggy and doesn't return the full list of software
if len(chocos) < 4000:
continue
else:
logger.info(f"Chocos were updated using {agent.salt_id}")
ChocoSoftware(chocos=chocos).save()
break
break
return "ok"
@app.task
def install_program(pk, name, version):
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_cmd(
timeout=900,
func="chocolatey.install",
arg=[name, f"version={version}"],
)
if r == "timeout" or r == "error":
nats_data = {
"func": "installwithchoco",
"choco_prog_name": name,
"choco_prog_ver": version,
}
r: str = asyncio.run(agent.nats_cmd(nats_data, timeout=915))
if r == "timeout":
logger.error(f"Failed to install {name} {version} on {agent.salt_id}: timeout")
return

View File

@@ -2,8 +2,7 @@ from tacticalrmm.test import TacticalTestCase
from .serializers import InstalledSoftwareSerializer
from model_bakery import baker
from unittest.mock import patch
from .models import InstalledSoftware, ChocoLog
from agents.models import Agent
from .models import ChocoLog
class TestSoftwareViews(TacticalTestCase):
@@ -64,83 +63,20 @@ class TestSoftwareViews(TacticalTestCase):
class TestSoftwareTasks(TacticalTestCase):
@patch("agents.models.Agent.salt_api_cmd")
def test_install_chocolatey(self, salt_api_cmd):
from .tasks import install_chocolatey
agent = baker.make_recipe("agents.agent")
# test failed attempt
salt_api_cmd.return_value = "timeout"
ret = install_chocolatey(agent.pk)
salt_api_cmd.assert_called_with(
timeout=120, func="chocolatey.bootstrap", arg="force=True"
)
self.assertFalse(ret)
# test successful
salt_api_cmd.return_value = "chocolatey is now ready"
ret = install_chocolatey(agent.pk)
salt_api_cmd.assert_called_with(
timeout=120, func="chocolatey.bootstrap", arg="force=True"
)
self.assertTrue(ret)
self.assertTrue(Agent.objects.get(pk=agent.pk).choco_installed)
@patch("agents.models.Agent.salt_api_cmd")
def test_update_chocos(self, salt_api_cmd):
from .tasks import update_chocos
# initialize data
online_agent = baker.make_recipe("agents.online_agent", choco_installed=True)
baker.make("software.ChocoSoftware", chocos={})
# return data
chocolately_list = {
"git": "2.3.4",
"docker": "1.0.2",
}
# test failed attempt
salt_api_cmd.return_value = "timeout"
ret = update_chocos()
salt_api_cmd.assert_called_with(timeout=10, func="test.ping")
self.assertTrue(ret)
self.assertEquals(salt_api_cmd.call_count, 1)
salt_api_cmd.reset_mock()
# test successful attempt
salt_api_cmd.side_effect = [True, chocolately_list]
ret = update_chocos()
self.assertTrue(ret)
salt_api_cmd.assert_any_call(timeout=10, func="test.ping")
salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list")
self.assertEquals(salt_api_cmd.call_count, 2)
@patch("agents.models.Agent.salt_api_cmd")
def test_install_program(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_install_program(self, nats_cmd):
from .tasks import install_program
agent = baker.make_recipe("agents.agent")
# failed attempt
salt_api_cmd.return_value = "timeout"
ret = install_program(agent.pk, "git", "2.3.4")
self.assertFalse(ret)
salt_api_cmd.assert_called_with(
timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"]
)
salt_api_cmd.reset_mock()
# successfully attempt
salt_api_cmd.return_value = "install of git was successful"
ret = install_program(agent.pk, "git", "2.3.4")
self.assertTrue(ret)
salt_api_cmd.assert_called_with(
timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"]
nats_cmd.return_value = "install of git was successful"
_ = install_program(agent.pk, "git", "2.3.4")
nats_cmd.assert_called_with(
{
"func": "installwithchoco",
"choco_prog_name": "git",
"choco_prog_ver": "2.3.4",
},
timeout=915,
)
self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists())

View File

@@ -8,14 +8,15 @@ from rest_framework.response import Response
from agents.models import Agent
from .models import ChocoSoftware, InstalledSoftware
from .serializers import InstalledSoftwareSerializer
from .serializers import InstalledSoftwareSerializer, ChocoSoftwareSerializer
from .tasks import install_program
from tacticalrmm.utils import notify_error, filter_software
@api_view()
def chocos(request):
return Response(ChocoSoftware.combine_all())
chocos = ChocoSoftware.objects.last()
return Response(ChocoSoftwareSerializer(chocos).data["chocos"])
@api_view(["POST"])

View File

@@ -21,10 +21,6 @@ app.conf.task_track_started = True
app.autodiscover_tasks()
app.conf.beat_schedule = {
"update-chocos": {
"task": "software.tasks.update_chocos",
"schedule": crontab(minute=0, hour=4),
},
"auto-approve-win-updates": {
"task": "winupdate.tasks.auto_approve_updates_task",
"schedule": crontab(minute=2, hour="*/8"),
@@ -33,21 +29,13 @@ app.conf.beat_schedule = {
"task": "winupdate.tasks.check_agent_update_schedule_task",
"schedule": crontab(minute=5, hour="*"),
},
"agents-checkinfull": {
"task": "agents.tasks.check_in_task",
"schedule": crontab(minute="*/24"),
},
"agent-auto-update": {
"task": "agents.tasks.auto_self_agent_update_task",
"schedule": crontab(minute=35, hour="*"),
},
"agents-sync": {
"task": "agents.tasks.sync_sysinfo_task",
"schedule": crontab(minute=55, hour="*"),
},
"check-agentservice": {
"task": "agents.tasks.monitor_agents_task",
"schedule": crontab(minute="*/15"),
"remove-salt": {
"task": "agents.tasks.remove_salt_task",
"schedule": crontab(minute=14, hour="*/2"),
},
}

View File

@@ -37,10 +37,7 @@ if not DEBUG:
)
})
SALT_USERNAME = "changeme"
SALT_PASSWORD = "changeme"
MESH_USERNAME = "changeme"
MESH_SITE = "https://mesh.example.com"
MESH_TOKEN_KEY = "changeme"
REDIS_HOST = "localhost"
SALT_HOST = "127.0.0.1"
REDIS_HOST = "localhost"

View File

@@ -14,8 +14,8 @@ def get_debug_info():
EXCLUDE_PATHS = (
"/natsapi",
"/api/v3",
"/api/v2",
"/logs/auditlogs",
f"/{settings.ADMIN_URL}",
"/logout",

View File

@@ -15,32 +15,24 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.2.21"
TRMM_VERSION = "0.4.3"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.102"
# https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.1.0"
APP_VER = "0.0.109"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.1.12"
LATEST_AGENT_VER = "1.4.1"
MESH_VER = "0.7.37"
SALT_MASTER_VER = "3002.2"
MESH_VER = "0.7.54"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "5"
NPM_VER = "5"
PIP_VER = "8"
NPM_VER = "7"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
SALT_64 = f"https://github.com/wh1te909/salt/releases/download/{LATEST_SALT_VER}/salt-minion-setup.exe"
SALT_32 = f"https://github.com/wh1te909/salt/releases/download/{LATEST_SALT_VER}/salt-minion-setup-x86.exe"
try:
from .local_settings import *
except ImportError:
@@ -58,7 +50,6 @@ INSTALLED_APPS = [
"knox",
"corsheaders",
"accounts",
"apiv2",
"apiv3",
"clients",
"agents",
@@ -72,6 +63,7 @@ INSTALLED_APPS = [
"logs",
"scripts",
"alerts",
"natsapi",
]
if not "TRAVIS" in os.environ and not "AZPIPELINE" in os.environ:
@@ -175,17 +167,14 @@ if "AZPIPELINE" in os.environ:
}
ALLOWED_HOSTS = ["api.example.com"]
DOCKER_BUILD = True
DEBUG = True
SECRET_KEY = "abcdefghijklmnoptravis123456789"
ADMIN_URL = "abc123456/"
SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts")
SALT_USERNAME = "pipeline"
SALT_PASSWORD = "pipeline"
MESH_USERNAME = "pipeline"
MESH_SITE = "https://example.com"
MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
REDIS_HOST = "localhost"
SALT_HOST = "127.0.0.1"
KEEP_SALT = False

View File

@@ -2,7 +2,7 @@
{
"name": "System",
"cpu_percent": 0.0,
"memory_percent": 7.754021906781984e-05,
"membytes": 434655234324,
"pid": 4,
"ppid": 0,
"status": "running",
@@ -12,7 +12,7 @@
{
"name": "Registry",
"cpu_percent": 0.0,
"memory_percent": 0.009720362333912082,
"membytes": 0.009720362333912082,
"pid": 280,
"ppid": 4,
"status": "running",
@@ -22,7 +22,7 @@
{
"name": "smss.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0006223099632878874,
"membytes": 0.0006223099632878874,
"pid": 976,
"ppid": 4,
"status": "running",
@@ -32,7 +32,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005682306310149464,
"membytes": 0.005682306310149464,
"pid": 1160,
"ppid": 1388,
"status": "running",
@@ -42,7 +42,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004793576106987529,
"membytes": 0.004793576106987529,
"pid": 1172,
"ppid": 1388,
"status": "running",
@@ -52,7 +52,7 @@
{
"name": "csrss.exe",
"cpu_percent": 0.0,
"memory_percent": 0.002459416691971619,
"membytes": 0.002459416691971619,
"pid": 1240,
"ppid": 1220,
"status": "running",
@@ -62,7 +62,7 @@
{
"name": "wininit.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0031970428784885716,
"membytes": 0.0031970428784885716,
"pid": 1316,
"ppid": 1220,
"status": "running",
@@ -72,7 +72,7 @@
{
"name": "csrss.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0023719354191771556,
"membytes": 0.0023719354191771556,
"pid": 1324,
"ppid": 1308,
"status": "running",
@@ -82,7 +82,7 @@
{
"name": "services.exe",
"cpu_percent": 0.0,
"memory_percent": 0.00596662044673147,
"membytes": 0.00596662044673147,
"pid": 1388,
"ppid": 1316,
"status": "running",
@@ -92,7 +92,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006052113508780605,
"membytes": 0.006052113508780605,
"pid": 1396,
"ppid": 1388,
"status": "running",
@@ -102,7 +102,7 @@
{
"name": "LsaIso.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0016124389144615866,
"membytes": 0.0016124389144615866,
"pid": 1408,
"ppid": 1316,
"status": "running",
@@ -112,7 +112,7 @@
{
"name": "lsass.exe",
"cpu_percent": 0.0,
"memory_percent": 0.012698702030414497,
"membytes": 0.012698702030414497,
"pid": 1416,
"ppid": 1316,
"status": "running",
@@ -122,7 +122,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.007129723732748768,
"membytes": 0.007129723732748768,
"pid": 1444,
"ppid": 1388,
"status": "running",
@@ -132,7 +132,7 @@
{
"name": "winlogon.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005396003962822129,
"membytes": 0.005396003962822129,
"pid": 1492,
"ppid": 1308,
"status": "running",
@@ -142,7 +142,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0027815068327148706,
"membytes": 0.0027815068327148706,
"pid": 1568,
"ppid": 1388,
"status": "running",
@@ -152,7 +152,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.001936517265950167,
"membytes": 0.001936517265950167,
"pid": 1604,
"ppid": 1388,
"status": "running",
@@ -162,7 +162,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011187661863964672,
"membytes": 0.011187661863964672,
"pid": 1628,
"ppid": 1388,
"status": "running",
@@ -172,7 +172,7 @@
{
"name": "fontdrvhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.002765601146752241,
"membytes": 0.002765601146752241,
"pid": 1652,
"ppid": 1492,
"status": "running",
@@ -182,7 +182,7 @@
{
"name": "fontdrvhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0017794486170691988,
"membytes": 0.0017794486170691988,
"pid": 1660,
"ppid": 1316,
"status": "running",
@@ -192,7 +192,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006676411682813821,
"membytes": 0.006676411682813821,
"pid": 1752,
"ppid": 1388,
"status": "running",
@@ -202,7 +202,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004892986644253965,
"membytes": 0.004892986644253965,
"pid": 1796,
"ppid": 1388,
"status": "running",
@@ -212,7 +212,7 @@
{
"name": "dwm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.02493216274642207,
"membytes": 0.02493216274642207,
"pid": 1868,
"ppid": 1492,
"status": "running",
@@ -222,7 +222,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011945170157934911,
"membytes": 0.011945170157934911,
"pid": 1972,
"ppid": 1388,
"status": "running",
@@ -232,7 +232,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006616765360453959,
"membytes": 0.006616765360453959,
"pid": 1980,
"ppid": 1388,
"status": "running",
@@ -242,7 +242,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0034435810109093323,
"membytes": 0.0034435810109093323,
"pid": 2008,
"ppid": 1388,
"status": "running",
@@ -252,7 +252,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004722000520155695,
"membytes": 0.004722000520155695,
"pid": 2160,
"ppid": 1388,
"status": "running",
@@ -262,7 +262,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004264712048730091,
"membytes": 0.004264712048730091,
"pid": 2196,
"ppid": 1388,
"status": "running",
@@ -272,7 +272,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005493426289343236,
"membytes": 0.005493426289343236,
"pid": 2200,
"ppid": 1388,
"status": "running",
@@ -282,7 +282,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.002757648303770926,
"membytes": 0.002757648303770926,
"pid": 2212,
"ppid": 1388,
"status": "running",
@@ -292,7 +292,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0038113999987951447,
"membytes": 0.0038113999987951447,
"pid": 2224,
"ppid": 1388,
"status": "running",
@@ -302,7 +302,7 @@
{
"name": "mmc.exe",
"cpu_percent": 0.084375,
"memory_percent": 0.027600341566653204,
"membytes": 0.027600341566653204,
"pid": 2272,
"ppid": 4664,
"status": "running",
@@ -312,7 +312,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004185183618916942,
"membytes": 0.004185183618916942,
"pid": 2312,
"ppid": 1388,
"status": "running",
@@ -322,7 +322,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.003334229419916253,
"membytes": 0.003334229419916253,
"pid": 2352,
"ppid": 1388,
"status": "running",
@@ -332,7 +332,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.003841223159975075,
"membytes": 0.003841223159975075,
"pid": 2400,
"ppid": 1388,
"status": "running",
@@ -342,7 +342,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.00720527574107126,
"membytes": 0.00720527574107126,
"pid": 2440,
"ppid": 1388,
"status": "running",
@@ -352,7 +352,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.008088041311997208,
"membytes": 0.008088041311997208,
"pid": 2512,
"ppid": 1388,
"status": "running",
@@ -362,7 +362,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005859257066483719,
"membytes": 0.005859257066483719,
"pid": 2600,
"ppid": 1388,
"status": "running",
@@ -372,7 +372,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004566920082020056,
"membytes": 0.004566920082020056,
"pid": 2724,
"ppid": 1388,
"status": "running",
@@ -382,7 +382,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004475462387734934,
"membytes": 0.004475462387734934,
"pid": 2732,
"ppid": 1388,
"status": "running",
@@ -392,7 +392,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004006244651837358,
"membytes": 0.004006244651837358,
"pid": 2748,
"ppid": 1388,
"status": "running",
@@ -402,7 +402,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.003240783514885803,
"membytes": 0.003240783514885803,
"pid": 2796,
"ppid": 1388,
"status": "running",
@@ -412,7 +412,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0036404138746968747,
"membytes": 0.0036404138746968747,
"pid": 2852,
"ppid": 1388,
"status": "running",
@@ -422,7 +422,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005932820864060882,
"membytes": 0.005932820864060882,
"pid": 2936,
"ppid": 1388,
"status": "running",
@@ -432,7 +432,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004240853519786147,
"membytes": 0.004240853519786147,
"pid": 2944,
"ppid": 1388,
"status": "running",
@@ -442,7 +442,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.009068229209444265,
"membytes": 0.009068229209444265,
"pid": 2952,
"ppid": 1388,
"status": "running",
@@ -452,7 +452,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.008205345745971602,
"membytes": 0.008205345745971602,
"pid": 3036,
"ppid": 1388,
"status": "running",
@@ -462,7 +462,7 @@
{
"name": "spaceman.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0003360076159605526,
"membytes": 0.0003360076159605526,
"pid": 3112,
"ppid": 2440,
"status": "running",
@@ -472,7 +472,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.00409571413537715,
"membytes": 0.00409571413537715,
"pid": 3216,
"ppid": 1388,
"status": "running",
@@ -482,7 +482,7 @@
{
"name": "ShellExperienceHost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.030085604998314096,
"membytes": 0.030085604998314096,
"pid": 3228,
"ppid": 1628,
"status": "running",
@@ -492,7 +492,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004664342408541163,
"membytes": 0.004664342408541163,
"pid": 3244,
"ppid": 1388,
"status": "running",
@@ -502,7 +502,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004843281375620747,
"membytes": 0.004843281375620747,
"pid": 3268,
"ppid": 1388,
"status": "running",
@@ -512,7 +512,7 @@
{
"name": "python.exe",
"cpu_percent": 0.559375,
"memory_percent": 0.029455342192044896,
"membytes": 0.029455342192044896,
"pid": 3288,
"ppid": 4708,
"status": "running",
@@ -522,7 +522,7 @@
{
"name": "RuntimeBroker.exe",
"cpu_percent": 0.0,
"memory_percent": 0.010283025974840107,
"membytes": 0.010283025974840107,
"pid": 3296,
"ppid": 1628,
"status": "running",
@@ -532,7 +532,7 @@
{
"name": "RuntimeBroker.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006596883253000673,
"membytes": 0.006596883253000673,
"pid": 3308,
"ppid": 1628,
"status": "running",
@@ -542,7 +542,7 @@
{
"name": "spoolsv.exe",
"cpu_percent": 0.0,
"memory_percent": 0.008095994154978522,
"membytes": 0.008095994154978522,
"pid": 3708,
"ppid": 1388,
"status": "running",
@@ -552,7 +552,7 @@
{
"name": "conhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011507763793962596,
"membytes": 0.011507763793962596,
"pid": 3752,
"ppid": 6620,
"status": "running",
@@ -562,7 +562,7 @@
{
"name": "LogMeInSystray.exe",
"cpu_percent": 0.0,
"memory_percent": 0.010300919871548067,
"membytes": 0.010300919871548067,
"pid": 3780,
"ppid": 4664,
"status": "running",
@@ -572,7 +572,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005767799372198599,
"membytes": 0.005767799372198599,
"pid": 3808,
"ppid": 1388,
"status": "running",
@@ -582,7 +582,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.007070077410388906,
"membytes": 0.007070077410388906,
"pid": 3816,
"ppid": 1388,
"status": "running",
@@ -592,7 +592,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.014217695039845633,
"membytes": 0.014217695039845633,
"pid": 3824,
"ppid": 1388,
"status": "running",
@@ -602,7 +602,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.022611920806623463,
"membytes": 0.022611920806623463,
"pid": 3832,
"ppid": 1388,
"status": "running",
@@ -612,7 +612,7 @@
{
"name": "nssm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.003163243295817984,
"membytes": 0.003163243295817984,
"pid": 3840,
"ppid": 1388,
"status": "running",
@@ -622,7 +622,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0030717856015328626,
"membytes": 0.0030717856015328626,
"pid": 3856,
"ppid": 1388,
"status": "running",
@@ -632,7 +632,7 @@
{
"name": "LMIGuardianSvc.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004441662805064347,
"membytes": 0.004441662805064347,
"pid": 3868,
"ppid": 1388,
"status": "running",
@@ -642,7 +642,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0026781198739577773,
"membytes": 0.0026781198739577773,
"pid": 3876,
"ppid": 1388,
"status": "running",
@@ -652,7 +652,7 @@
{
"name": "ramaint.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0038471877922110613,
"membytes": 0.0038471877922110613,
"pid": 3884,
"ppid": 1388,
"status": "running",
@@ -662,7 +662,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005374133644623514,
"membytes": 0.005374133644623514,
"pid": 3892,
"ppid": 1388,
"status": "running",
@@ -672,7 +672,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006421920707411746,
"membytes": 0.006421920707411746,
"pid": 3900,
"ppid": 1388,
"status": "running",
@@ -682,7 +682,7 @@
{
"name": "ssm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0031612550850726546,
"membytes": 0.0031612550850726546,
"pid": 3908,
"ppid": 1388,
"status": "running",
@@ -692,7 +692,7 @@
{
"name": "MeshAgent.exe",
"cpu_percent": 0.0,
"memory_percent": 0.01894963661372797,
"membytes": 0.01894963661372797,
"pid": 3920,
"ppid": 1388,
"status": "running",
@@ -702,7 +702,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006905055918526623,
"membytes": 0.006905055918526623,
"pid": 4076,
"ppid": 1388,
"status": "running",
@@ -712,7 +712,7 @@
{
"name": "sihost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.012527715906316225,
"membytes": 0.012527715906316225,
"pid": 4136,
"ppid": 3268,
"status": "running",
@@ -722,7 +722,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004169277932954313,
"membytes": 0.004169277932954313,
"pid": 4160,
"ppid": 1388,
"status": "running",
@@ -732,7 +732,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006851374228402747,
"membytes": 0.006851374228402747,
"pid": 4192,
"ppid": 1388,
"status": "running",
@@ -742,7 +742,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006024278558346003,
"membytes": 0.006024278558346003,
"pid": 4208,
"ppid": 1388,
"status": "running",
@@ -752,7 +752,7 @@
{
"name": "LogMeIn.exe",
"cpu_percent": 0.0,
"memory_percent": 0.017691099211934895,
"membytes": 0.017691099211934895,
"pid": 4232,
"ppid": 1388,
"status": "running",
@@ -762,7 +762,7 @@
{
"name": "vmms.exe",
"cpu_percent": 0.0,
"memory_percent": 0.017331233067030397,
"membytes": 0.017331233067030397,
"pid": 4292,
"ppid": 1388,
"status": "running",
@@ -772,7 +772,7 @@
{
"name": "TabTip32.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0023441004687425535,
"membytes": 0.0023441004687425535,
"pid": 4304,
"ppid": 5916,
"status": "running",
@@ -782,7 +782,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.022273924979917578,
"membytes": 0.022273924979917578,
"pid": 4436,
"ppid": 1388,
"status": "running",
@@ -792,7 +792,7 @@
{
"name": "explorer.exe",
"cpu_percent": 0.0,
"memory_percent": 0.040491900039364585,
"membytes": 0.040491900039364585,
"pid": 4664,
"ppid": 2804,
"status": "running",
@@ -802,7 +802,7 @@
{
"name": "tacticalrmm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.019854272502852533,
"membytes": 0.019854272502852533,
"pid": 4696,
"ppid": 3840,
"status": "running",
@@ -812,7 +812,7 @@
{
"name": "python.exe",
"cpu_percent": 0.0,
"memory_percent": 0.03651547854870715,
"membytes": 0.03651547854870715,
"pid": 4708,
"ppid": 3908,
"status": "running",
@@ -822,7 +822,7 @@
{
"name": "conhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0060938659344325075,
"membytes": 0.0060938659344325075,
"pid": 4728,
"ppid": 4708,
"status": "running",
@@ -832,7 +832,7 @@
{
"name": "conhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006127665517103096,
"membytes": 0.006127665517103096,
"pid": 4736,
"ppid": 4696,
"status": "running",
@@ -842,7 +842,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0035111801762505086,
"membytes": 0.0035111801762505086,
"pid": 4752,
"ppid": 1388,
"status": "running",
@@ -852,7 +852,7 @@
{
"name": "vmcompute.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005598801458845658,
"membytes": 0.005598801458845658,
"pid": 5020,
"ppid": 1388,
"status": "running",
@@ -862,7 +862,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005260805632139777,
"membytes": 0.005260805632139777,
"pid": 5088,
"ppid": 1388,
"status": "running",
@@ -872,7 +872,7 @@
{
"name": "vmwp.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011384494727752215,
"membytes": 0.011384494727752215,
"pid": 5276,
"ppid": 5020,
"status": "running",
@@ -882,7 +882,7 @@
{
"name": "python.exe",
"cpu_percent": 0.0,
"memory_percent": 0.020685344594399937,
"membytes": 0.020685344594399937,
"pid": 5472,
"ppid": 4708,
"status": "running",
@@ -892,7 +892,7 @@
{
"name": "WmiPrvSE.exe",
"cpu_percent": 0.0,
"memory_percent": 0.010167709751611041,
"membytes": 0.010167709751611041,
"pid": 5712,
"ppid": 1628,
"status": "running",
@@ -902,7 +902,7 @@
{
"name": "TabTip.exe",
"cpu_percent": 0.0,
"memory_percent": 0.008543341572677483,
"membytes": 0.008543341572677483,
"pid": 5916,
"ppid": 4752,
"status": "running",
@@ -912,7 +912,7 @@
{
"name": "vmwp.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011780148666072628,
"membytes": 0.011780148666072628,
"pid": 5924,
"ppid": 5020,
"status": "running",
@@ -922,7 +922,7 @@
{
"name": "msdtc.exe",
"cpu_percent": 0.0,
"memory_percent": 0.004956609388104484,
"membytes": 0.004956609388104484,
"pid": 6016,
"ppid": 1388,
"status": "running",
@@ -932,7 +932,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0025468979647660824,
"membytes": 0.0025468979647660824,
"pid": 6056,
"ppid": 1388,
"status": "running",
@@ -942,7 +942,7 @@
{
"name": "vmwp.exe",
"cpu_percent": 0.06875,
"memory_percent": 0.01141034146744149,
"membytes": 0.01141034146744149,
"pid": 6092,
"ppid": 5020,
"status": "running",
@@ -952,7 +952,7 @@
{
"name": "vmwp.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011595245066757059,
"membytes": 0.011595245066757059,
"pid": 6296,
"ppid": 5020,
"status": "running",
@@ -962,7 +962,7 @@
{
"name": "cmd.exe",
"cpu_percent": 0.0,
"memory_percent": 0.00203990422470726,
"membytes": 0.00203990422470726,
"pid": 6620,
"ppid": 4664,
"status": "running",
@@ -972,7 +972,7 @@
{
"name": "ctfmon.exe",
"cpu_percent": 0.0,
"memory_percent": 0.007632741051316932,
"membytes": 0.007632741051316932,
"pid": 6648,
"ppid": 4752,
"status": "running",
@@ -982,7 +982,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.007199311108835272,
"membytes": 0.007199311108835272,
"pid": 6716,
"ppid": 1388,
"status": "running",
@@ -992,7 +992,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.0038054353665591583,
"membytes": 0.0038054353665591583,
"pid": 6760,
"ppid": 1388,
"status": "running",
@@ -1002,7 +1002,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.013456210324384736,
"membytes": 0.013456210324384736,
"pid": 6868,
"ppid": 1388,
"status": "running",
@@ -1012,7 +1012,7 @@
{
"name": "SearchUI.exe",
"cpu_percent": 0.0,
"memory_percent": 0.04596743243199986,
"membytes": 0.04596743243199986,
"pid": 6904,
"ppid": 1628,
"status": "stopped",
@@ -1022,7 +1022,7 @@
{
"name": "tacticalrmm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.023025468641651836,
"membytes": 0.023025468641651836,
"pid": 6908,
"ppid": 7592,
"status": "running",
@@ -1032,7 +1032,7 @@
{
"name": "taskhostw.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006147547624556384,
"membytes": 0.006147547624556384,
"pid": 6984,
"ppid": 2440,
"status": "running",
@@ -1042,7 +1042,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.017520113087836627,
"membytes": 0.017520113087836627,
"pid": 7092,
"ppid": 1388,
"status": "running",
@@ -1052,7 +1052,7 @@
{
"name": "RuntimeBroker.exe",
"cpu_percent": 0.0,
"memory_percent": 0.011543551587378511,
"membytes": 0.011543551587378511,
"pid": 7148,
"ppid": 1628,
"status": "running",
@@ -1062,7 +1062,7 @@
{
"name": "dllhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006175382574990985,
"membytes": 0.006175382574990985,
"pid": 7232,
"ppid": 1628,
"status": "running",
@@ -1072,7 +1072,7 @@
{
"name": "conhost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.006191288260953614,
"membytes": 0.006191288260953614,
"pid": 7288,
"ppid": 6908,
"status": "running",
@@ -1082,7 +1082,7 @@
{
"name": "nssm.exe",
"cpu_percent": 0.0,
"memory_percent": 0.003252712779357776,
"membytes": 0.003252712779357776,
"pid": 7592,
"ppid": 1388,
"status": "running",
@@ -1092,7 +1092,7 @@
{
"name": "svchost.exe",
"cpu_percent": 0.0,
"memory_percent": 0.005972585078967456,
"membytes": 0.005972585078967456,
"pid": 8012,
"ppid": 1388,
"status": "running",

View File

@@ -10,7 +10,6 @@ urlpatterns = [
path("login/", LoginView.as_view()),
path("logout/", knox_views.LogoutView.as_view()),
path("logoutall/", knox_views.LogoutAllView.as_view()),
path("api/v2/", include("apiv2.urls")),
path("api/v3/", include("apiv3.urls")),
path("clients/", include("clients.urls")),
path("agents/", include("agents.urls")),
@@ -25,4 +24,5 @@ urlpatterns = [
path("scripts/", include("scripts.urls")),
path("alerts/", include("alerts.urls")),
path("accounts/", include("accounts.urls")),
path("natsapi/", include("natsapi.urls")),
]

View File

@@ -0,0 +1,93 @@
# Generated by Django 3.1.5 on 2021-01-19 00:52
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("winupdate", "0009_auto_20200922_1344"),
]
operations = [
migrations.AddField(
model_name="winupdate",
name="categories",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(blank=True, max_length=255, null=True),
blank=True,
default=list,
null=True,
size=None,
),
),
migrations.AddField(
model_name="winupdate",
name="category_ids",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(blank=True, max_length=255, null=True),
blank=True,
default=list,
null=True,
size=None,
),
),
migrations.AddField(
model_name="winupdate",
name="kb_article_ids",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(blank=True, max_length=255, null=True),
blank=True,
default=list,
null=True,
size=None,
),
),
migrations.AddField(
model_name="winupdate",
name="more_info_urls",
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="winupdate",
name="revision_number",
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="winupdate",
name="support_url",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="winupdate",
name="date_installed",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name="winupdate",
name="description",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="winupdate",
name="guid",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="winupdate",
name="kb",
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AlterField(
model_name="winupdate",
name="title",
field=models.TextField(blank=True, null=True),
),
]

View File

@@ -42,20 +42,46 @@ class WinUpdate(models.Model):
agent = models.ForeignKey(
Agent, related_name="winupdates", on_delete=models.CASCADE
)
guid = models.CharField(max_length=255, null=True)
kb = models.CharField(max_length=100, null=True)
mandatory = models.BooleanField(default=False)
title = models.TextField(null=True)
needs_reboot = models.BooleanField(default=False)
guid = models.CharField(max_length=255, null=True, blank=True)
kb = models.CharField(max_length=100, null=True, blank=True)
mandatory = models.BooleanField(default=False) # deprecated
title = models.TextField(null=True, blank=True)
needs_reboot = models.BooleanField(default=False) # deprecated
installed = models.BooleanField(default=False)
downloaded = models.BooleanField(default=False)
description = models.TextField(null=True)
description = models.TextField(null=True, blank=True)
severity = models.CharField(max_length=255, null=True, blank=True)
categories = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
category_ids = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
kb_article_ids = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
more_info_urls = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
support_url = models.TextField(null=True, blank=True)
revision_number = models.IntegerField(null=True, blank=True)
action = models.CharField(
max_length=100, choices=PATCH_ACTION_CHOICES, default="nothing"
)
result = models.CharField(max_length=255, default="n/a")
date_installed = models.DateTimeField(null=True)
date_installed = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"{self.agent.hostname} {self.kb}"

View File

@@ -1,9 +1,12 @@
from time import sleep
import asyncio
import time
from django.utils import timezone as djangotime
from django.conf import settings
import datetime as dt
import pytz
from loguru import logger
from packaging import version as pyver
from typing import List
from agents.models import Agent
from .models import WinUpdate
@@ -16,31 +19,42 @@ logger.configure(**settings.LOG_CONFIG)
def auto_approve_updates_task():
# scheduled task that checks and approves updates daily
agents = Agent.objects.all()
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
for agent in agents:
agent.delete_superseded_updates()
try:
agent.approve_updates()
except:
continue
online = [i for i in agents if i.status == "online"]
online = [
i
for i in agents
if i.status == "online" and pyver.parse(i.version) >= pyver.parse("1.3.0")
]
for agent in online:
# check for updates on agent
check_for_updates_task.apply_async(
queue="wupdate",
kwargs={"pk": agent.pk, "wait": False, "auto_approve": True},
)
chunks = (online[i : i + 40] for i in range(0, len(online), 40))
for chunk in chunks:
for agent in chunk:
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
time.sleep(0.05)
time.sleep(15)
@app.task
def check_agent_update_schedule_task():
# scheduled task that installs updates on agents if enabled
agents = Agent.objects.all()
online = [i for i in agents if i.has_patches_pending and i.status == "online"]
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.3.0")
and i.has_patches_pending
and i.status == "online"
]
for agent in online:
agent.delete_superseded_updates()
install = False
patch_policy = agent.get_patch_policy()
@@ -98,117 +112,40 @@ def check_agent_update_schedule_task():
if install:
# initiate update on agent asynchronously and don't worry about ret code
logger.info(f"Installing windows updates on {agent.salt_id}")
agent.salt_api_async(func="win_agent.install_updates")
nats_data = {
"func": "installwinupdates",
"guids": agent.get_approved_update_guids(),
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
agent.patches_last_installed = djangotime.now()
agent.save(update_fields=["patches_last_installed"])
@app.task
def check_for_updates_task(pk, wait=False, auto_approve=False):
if wait:
sleep(120)
agent = Agent.objects.get(pk=pk)
ret = agent.salt_api_cmd(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
if ret == "timeout" or ret == "error":
return
if isinstance(ret, str):
err = ["unknown failure", "2147352567", "2145107934"]
if any(x in ret.lower() for x in err):
logger.warning(f"{agent.salt_id}: {ret}")
return "failed"
guids = []
try:
for k in ret.keys():
guids.append(k)
except Exception as e:
logger.error(f"{agent.salt_id}: {str(e)}")
return
for i in guids:
# check if existing update install / download status has changed
if WinUpdate.objects.filter(agent=agent).filter(guid=i).exists():
update = WinUpdate.objects.filter(agent=agent).get(guid=i)
# salt will report an update as not installed even if it has been installed if a reboot is pending
# ignore salt's return if the result field is 'success' as that means the agent has successfully installed the update
if update.result != "success":
if ret[i]["Installed"] != update.installed:
update.installed = not update.installed
update.save(update_fields=["installed"])
if ret[i]["Downloaded"] != update.downloaded:
update.downloaded = not update.downloaded
update.save(update_fields=["downloaded"])
# otherwise it's a new update
else:
WinUpdate(
agent=agent,
guid=i,
kb=ret[i]["KBs"][0],
mandatory=ret[i]["Mandatory"],
title=ret[i]["Title"],
needs_reboot=ret[i]["NeedsReboot"],
installed=ret[i]["Installed"],
downloaded=ret[i]["Downloaded"],
description=ret[i]["Description"],
severity=ret[i]["Severity"],
).save()
agent.delete_superseded_updates()
# win_wua.list doesn't always return everything
# use win_wua.installed to check for any updates that it missed
# and then change update status to match
installed = agent.salt_api_cmd(
timeout=60, func="win_wua.installed", arg="kbs_only=True"
)
if installed == "timeout" or installed == "error":
pass
elif isinstance(installed, list):
agent.winupdates.filter(kb__in=installed).filter(installed=False).update(
installed=True, downloaded=True
)
# check if reboot needed. returns bool
needs_reboot = agent.salt_api_cmd(timeout=30, func="win_wua.get_needs_reboot")
if needs_reboot == "timeout" or needs_reboot == "error":
pass
elif isinstance(needs_reboot, bool) and needs_reboot:
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
else:
agent.needs_reboot = False
agent.save(update_fields=["needs_reboot"])
# approve updates if specified
if auto_approve:
agent.approve_updates()
return "ok"
def bulk_install_updates_task(pks: List[int]) -> None:
q = Agent.objects.filter(pk__in=pks)
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")]
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
for chunk in chunks:
for agent in chunk:
agent.delete_superseded_updates()
nats_data = {
"func": "installwinupdates",
"guids": agent.get_approved_update_guids(),
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
time.sleep(0.05)
time.sleep(15)
@app.task
def bulk_check_for_updates_task(minions):
# don't flood the celery queue
chunks = (minions[i : i + 30] for i in range(0, len(minions), 30))
def bulk_check_for_updates_task(pks: List[int]) -> None:
q = Agent.objects.filter(pk__in=pks)
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")]
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
for chunk in chunks:
for i in chunk:
agent = Agent.objects.get(salt_id=i)
check_for_updates_task.apply_async(
queue="wupdate",
kwargs={"pk": agent.pk, "wait": False, "auto_approve": True},
)
sleep(30)
for agent in chunk:
agent.delete_superseded_updates()
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
time.sleep(0.05)
time.sleep(15)

View File

@@ -29,7 +29,7 @@ class TestWinUpdateViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("winupdate.tasks.check_for_updates_task.apply_async")
""" @patch("winupdate.tasks.check_for_updates_task.apply_async")
def test_run_update_scan(self, mock_task):
# test a call where agent doesn't exist
@@ -46,9 +46,9 @@ class TestWinUpdateViews(TacticalTestCase):
kwargs={"pk": agent.pk, "wait": False, "auto_approve": True},
)
self.check_not_authenticated("get", url)
self.check_not_authenticated("get", url) """
@patch("agents.models.Agent.salt_api_cmd")
""" @patch("agents.models.Agent.salt_api_cmd")
def test_install_updates(self, mock_cmd):
# test a call where agent doesn't exist
@@ -84,7 +84,7 @@ class TestWinUpdateViews(TacticalTestCase):
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("get", url)
self.check_not_authenticated("get", url) """
def test_edit_policy(self):
url = "/winupdate/editpolicy/"
@@ -113,8 +113,9 @@ class WinupdateTasks(TacticalTestCase):
)
self.offline_agent = baker.make_recipe("agents.agent", site=site)
@patch("winupdate.tasks.check_for_updates_task.apply_async")
def test_auto_approve_task(self, check_updates_task):
@patch("agents.models.Agent.nats_cmd")
@patch("time.sleep")
def test_auto_approve_task(self, mock_sleep, nats_cmd):
from .tasks import auto_approve_updates_task
# Setup data
@@ -137,14 +138,14 @@ class WinupdateTasks(TacticalTestCase):
auto_approve_updates_task()
# make sure the check_for_updates_task was run once for each online agent
self.assertEqual(check_updates_task.call_count, 2)
self.assertEqual(nats_cmd.call_count, 2)
# check if all of the created updates were approved
winupdates = WinUpdate.objects.all()
for update in winupdates:
self.assertEqual(update.action, "approve")
@patch("agents.models.Agent.salt_api_async")
""" @patch("agents.models.Agent.salt_api_async")
def test_check_agent_update_daily_schedule(self, agent_salt_cmd):
from .tasks import check_agent_update_schedule_task
@@ -173,7 +174,7 @@ class WinupdateTasks(TacticalTestCase):
check_agent_update_schedule_task()
agent_salt_cmd.assert_called_with(func="win_agent.install_updates")
self.assertEquals(agent_salt_cmd.call_count, 2)
self.assertEquals(agent_salt_cmd.call_count, 2) """
""" @patch("agents.models.Agent.salt_api_async")
def test_check_agent_update_monthly_schedule(self, agent_salt_cmd):
@@ -205,109 +206,3 @@ class WinupdateTasks(TacticalTestCase):
check_agent_update_schedule_task()
agent_salt_cmd.assert_called_with(func="win_agent.install_updates")
self.assertEquals(agent_salt_cmd.call_count, 2) """
@patch("agents.models.Agent.salt_api_cmd")
def test_check_for_updates(self, salt_api_cmd):
from .tasks import check_for_updates_task
# create a matching update returned from salt
baker.make_recipe(
"winupdate.approved_winupdate",
agent=self.online_agents[0],
kb="KB12341234",
guid="GUID1",
downloaded=True,
severity="",
installed=True,
)
salt_success_return = {
"GUID1": {
"Title": "Update Title",
"KBs": ["KB12341234"],
"GUID": "GUID1",
"Description": "Description",
"Downloaded": False,
"Installed": False,
"Mandatory": False,
"Severity": "",
"NeedsReboot": True,
},
"GUID2": {
"Title": "Update Title 2",
"KBs": ["KB12341235"],
"GUID": "GUID2",
"Description": "Description",
"Downloaded": False,
"Installed": True,
"Mandatory": False,
"Severity": "",
"NeedsReboot": True,
},
}
salt_kb_list = ["KB12341235"]
# mock failed attempt
salt_api_cmd.return_value = "timeout"
ret = check_for_updates_task(self.online_agents[0].pk)
salt_api_cmd.assert_called_with(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
self.assertFalse(ret)
salt_api_cmd.reset_mock()
# mock failed attempt
salt_api_cmd.return_value = "error"
ret = check_for_updates_task(self.online_agents[0].pk)
salt_api_cmd.assert_called_with(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
self.assertFalse(ret)
salt_api_cmd.reset_mock()
# mock failed attempt
salt_api_cmd.return_value = "unknown failure"
ret = check_for_updates_task(self.online_agents[0].pk)
salt_api_cmd.assert_called_with(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
self.assertEquals(ret, "failed")
salt_api_cmd.reset_mock()
# mock failed attempt at salt list updates with reboot
salt_api_cmd.side_effect = [salt_success_return, "timeout", True]
ret = check_for_updates_task(self.online_agents[0].pk)
salt_api_cmd.assert_any_call(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
salt_api_cmd.assert_any_call(
timeout=60, func="win_wua.installed", arg="kbs_only=True"
)
salt_api_cmd.assert_any_call(timeout=30, func="win_wua.get_needs_reboot")
salt_api_cmd.reset_mock()
# mock successful attempt without reboot
salt_api_cmd.side_effect = [salt_success_return, salt_kb_list, False]
ret = check_for_updates_task(self.online_agents[0].pk)
salt_api_cmd.assert_any_call(
timeout=310,
func="win_wua.list",
arg="skip_installed=False",
)
salt_api_cmd.assert_any_call(
timeout=60, func="win_wua.installed", arg="kbs_only=True"
)
salt_api_cmd.assert_any_call(timeout=30, func="win_wua.get_needs_reboot")

View File

@@ -1,10 +1,8 @@
import asyncio
from packaging import version as pyver
from django.shortcuts import get_object_or_404
from rest_framework.decorators import (
api_view,
authentication_classes,
permission_classes,
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
@@ -12,7 +10,6 @@ from rest_framework.permissions import IsAuthenticated
from agents.models import Agent
from .models import WinUpdate
from .serializers import UpdateSerializer, ApprovedUpdateSerializer
from .tasks import check_for_updates_task
from tacticalrmm.utils import notify_error
@@ -25,30 +22,26 @@ def get_win_updates(request, pk):
@api_view()
def run_update_scan(request, pk):
agent = get_object_or_404(Agent, pk=pk)
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False, "auto_approve": True}
)
agent.delete_superseded_updates()
if pyver.parse(agent.version) < pyver.parse("1.3.0"):
return notify_error("Requires agent version 1.3.0 or greater")
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok")
@api_view()
def install_updates(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=15, func="win_agent.install_updates")
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error":
return notify_error("Something went wrong")
elif r == "running":
return notify_error(f"Updates are already being installed on {agent.hostname}")
# successful response: {'return': [{'SALT-ID': {'pid': 3316}}]}
try:
r["pid"]
except (KeyError):
return notify_error(str(r))
agent.delete_superseded_updates()
if pyver.parse(agent.version) < pyver.parse("1.3.0"):
return notify_error("Requires agent version 1.3.0 or greater")
nats_data = {
"func": "installwinupdates",
"guids": agent.get_approved_update_guids(),
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
return Response(f"Patches will now be installed on {agent.hostname}")

View File

@@ -27,7 +27,7 @@ jobs:
source env/bin/activate
cd /myagent/_work/1/s/api/tacticalrmm
pip install --no-cache-dir --upgrade pip
pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1
pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
displayName: "Install Python Dependencies"

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="5"
SCRIPT_VERSION="7"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
GREEN='\033[0;32m'
@@ -61,7 +61,6 @@ sysd="/etc/systemd/system"
mkdir -p ${tmp_dir}/meshcentral/mongo
mkdir ${tmp_dir}/postgres
mkdir ${tmp_dir}/salt
mkdir ${tmp_dir}/certs
mkdir ${tmp_dir}/nginx
mkdir ${tmp_dir}/systemd
@@ -74,16 +73,13 @@ pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432
tar -czvf ${tmp_dir}/meshcentral/mesh.tar.gz --exclude=/meshcentral/node_modules /meshcentral
mongodump --gzip --out=${tmp_dir}/meshcentral/mongo
sudo tar -czvf ${tmp_dir}/salt/etc-salt.tar.gz -C /etc/salt .
tar -czvf ${tmp_dir}/salt/srv-salt.tar.gz -C /srv/salt .
sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt .
sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/natsapi.service ${tmp_dir}/systemd/
cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz
cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/

View File

@@ -11,8 +11,8 @@ API_HOST=api.example.com
MESH_HOST=mesh.example.com
# mesh settings
MESH_USER=meshcentral
MESH_PASS=meshcentralpass
MESH_USER=tactical
MESH_PASS=tactical
MONGODB_USER=mongouser
MONGODB_PASSWORD=mongopass

View File

@@ -28,6 +28,10 @@ mesh_config="$(cat << EOF
"_AgentPing": 60,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"agentCoreDump": false,
"Compression": true,
"WsCompression": true,
"AgentWsCompression": true,
"MaxInvalidLogin": {
"time": 5,
"count": 5,
@@ -41,12 +45,7 @@ mesh_config="$(cat << EOF
"NewAccounts": false,
"mstsc": true,
"GeoLocation": true,
"CertUrl": "https://${NGINX_HOST_IP}:443",
"httpheaders": {
"Strict-Transport-Security": "max-age=360000",
"_x-frame-options": "sameorigin",
"Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; media-src 'self'"
}
"CertUrl": "https://${NGINX_HOST_IP}:443"
}
}
}

View File

@@ -7,6 +7,9 @@ RUN apk add --no-cache inotify-tools supervisor bash
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY natsapi/bin/nats-api /usr/local/bin/
RUN chmod +x /usr/local/bin/nats-api
COPY docker/containers/tactical-nats/entrypoint.sh /
RUN chmod +x /entrypoint.sh

View File

@@ -2,6 +2,15 @@
set -e
: "${DEV:=0}"
: "${API_CONTAINER:=tactical-backend}"
: "${API_PORT:=80}"
if [ "${DEV}" = 1 ]; then
NATS_CONFIG=/workspace/api/tacticalrmm/nats-rmm.conf
else
NATS_CONFIG="${TACTICAL_DIR}/api/nats-rmm.conf"
fi
sleep 15
until [ -f "${TACTICAL_READY_FILE}" ]; do
echo "waiting for init container to finish install or update..."
@@ -11,9 +20,6 @@ done
mkdir -p /var/log/supervisor
mkdir -p /etc/supervisor/conf.d
# wait for config changes
supervisor_config="$(cat << EOF
[supervisord]
nodaemon=true
@@ -21,13 +27,19 @@ nodaemon=true
files = /etc/supervisor/conf.d/*.conf
[program:nats-server]
command=nats-server --config ${TACTICAL_DIR}/api/nats-rmm.conf
command=nats-server --config ${NATS_CONFIG}
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:config-watcher]
command=/bin/bash -c "inotifywait -mq -e modify "${TACTICAL_DIR}/api/nats-rmm.conf" | while read event; do nats-server --signal reload; done;"
command=/bin/bash -c "inotifywait -mq -e modify "${NATS_CONFIG}" | while read event; do nats-server --signal reload; done;"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:nats-api]
command=/bin/bash -c "/usr/local/bin/nats-api -debug -api-host http://${API_CONTAINER}:${API_PORT}/natsapi -nats-host tls://${API_HOST}:4222"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
@@ -37,4 +49,4 @@ EOF
echo "${supervisor_config}" > /etc/supervisor/conf.d/supervisor.conf
# run supervised processes
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf

View File

@@ -2,6 +2,7 @@
set -e
: "${WORKER_CONNECTIONS:=2048}"
: "${APP_PORT:=80}"
: "${API_PORT:=80}"
@@ -25,6 +26,8 @@ else
fi
fi
/bin/bash -c "sed -i 's/worker_connections.*/worker_connections ${WORKER_CONNECTIONS};/g' /etc/nginx/nginx.conf"
nginx_config="$(cat << EOF
# backend config
server {
@@ -60,16 +63,8 @@ server {
alias ${TACTICAL_DIR}/api/tacticalrmm/private/;
}
location /saltscripts/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias ${TACTICAL_DIR}/scripts/userdefined/;
}
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias ${TACTICAL_DIR}/scripts/;
location ~ ^/(natsapi) {
deny all;
}
error_log /var/log/nginx/api-error.log;

View File

@@ -1,24 +0,0 @@
FROM ubuntu:20.04
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
ENV SALT_USER saltapi
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
RUN apt-get update && \
apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \
wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \
echo 'deb http://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest focal main' | tee /etc/apt/sources.list.d/saltstack.list && \
apt-get update && \
apt-get install -y salt-master salt-api && \
mkdir -p /var/log/supervisor && \
sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py && \
adduser --no-create-home --disabled-password --gecos "" ${SALT_USER}
EXPOSE 8123 4505 4506
COPY docker/containers/tactical-salt/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env bash
set -e
: "${SALT_USER:='saltapi'}"
sleep 15
until [ -f "${TACTICAL_READY_FILE}" ]; do
echo "waiting for init container to finish install or update..."
sleep 10
done
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
echo "${SALT_USER}:${SALT_PASS}" | chpasswd
cherrypy_config="$(cat << EOF
file_roots:
base:
- /srv/salt
- ${TACTICAL_DIR}
timeout: 20
gather_job_timeout: 25
max_event_size: 30485760
external_auth:
pam:
${SALT_USER}:
- .*
- '@runner'
- '@wheel'
- '@jobs'
rest_cherrypy:
port: 8123
disable_ssl: True
max_request_body_size: 30485760
EOF
)"
echo "${cherrypy_config}" > /etc/salt/master.d/rmm-salt.conf
supervisor_config="$(cat << EOF
[supervisord]
nodaemon=true
[include]
files = /etc/supervisor/conf.d/*.conf
[program:salt-master]
command=/bin/bash -c "salt-master -l info"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:salt-api]
command=/bin/bash -c "salt-api -l info"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
EOF
)"
echo "${supervisor_config}" > /etc/supervisor/conf.d/supervisor.conf
# run salt and salt master
/usr/bin/supervisord

View File

@@ -38,7 +38,6 @@ ENV PATH "${VIRTUAL_ENV}/bin:${TACTICAL_GO_DIR}/go/bin:$PATH"
# copy files from repo
COPY api/tacticalrmm ${TACTICAL_TMP_DIR}/api
COPY scripts ${TACTICAL_TMP_DIR}/scripts
COPY _modules ${TACTICAL_TMP_DIR}/_modules
# copy go install from build stage
COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go

View File

@@ -9,8 +9,6 @@ set -e
: "${POSTGRES_USER:=tactical}"
: "${POSTGRES_PASS:=tactical}"
: "${POSTGRES_DB:=tacticalrmm}"
: "${SALT_HOST:=tactical-salt}"
: "${SALT_USER:=saltapi}"
: "${MESH_CONTAINER:=tactical-meshcentral}"
: "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}"
@@ -53,14 +51,6 @@ if [ "$1" = 'tactical-init' ]; then
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
ADMINURL=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 70 | head -n 1)
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
# write salt pass to tmp dir
if [ ! -f "${TACTICAL__DIR}/tmp/salt_pass" ]; then
SALT_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
echo "${SALT_PASS}" > ${TACTICAL_DIR}/tmp/salt_pass
else
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
fi
localvars="$(cat << EOF
SECRET_KEY = '${DJANGO_SEKRET}'
@@ -74,7 +64,7 @@ KEY_FILE = '/opt/tactical/certs/privkey.pem'
SCRIPTS_DIR = '/opt/tactical/scripts'
ALLOWED_HOSTS = ['${API_HOST}']
ALLOWED_HOSTS = ['${API_HOST}', 'tactical-backend']
ADMIN_URL = '${ADMINURL}/'
@@ -111,9 +101,6 @@ if not DEBUG:
)
})
SALT_USERNAME = '${SALT_USER}'
SALT_PASSWORD = '${SALT_PASS}'
SALT_HOST = '${SALT_HOST}'
MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
@@ -168,16 +155,11 @@ fi
if [ "$1" = 'tactical-celery' ]; then
check_tactical_ready
celery -A tacticalrmm worker
celery -A tacticalrmm worker -l info
fi
if [ "$1" = 'tactical-celerybeat' ]; then
check_tactical_ready
test -f "${TACTICAL_DIR}/api/celerybeat.pid" && rm "${TACTICAL_DIR}/api/celerybeat.pid"
celery -A tacticalrmm beat
fi
if [ "$1" = 'tactical-celerywinupdate' ]; then
check_tactical_ready
celery -A tacticalrmm worker -Q wupdate
celery -A tacticalrmm beat -l info
fi

View File

@@ -15,7 +15,6 @@ networks:
# docker managed persistent volumes
volumes:
tactical_data:
salt_data:
postgres_data:
mongo_data:
mesh_data:
@@ -63,24 +62,13 @@ services:
- proxy
volumes:
- tactical_data:/opt/tactical
# salt master and api
tactical-salt:
image: ${IMAGE_REPO}tactical-salt:${VERSION}
restart: always
ports:
- "4505:4505"
- "4506:4506"
volumes:
- tactical_data:/opt/tactical
- salt_data:/etc/salt
networks:
- proxy
# nats
tactical-nats:
image: ${IMAGE_REPO}tactical-nats:${VERSION}
restart: always
environment:
API_HOST: ${API_HOST}
ports:
- "4222:4222"
volumes:
@@ -195,18 +183,3 @@ services:
depends_on:
- tactical-postgres
- tactical-redis
# container for celery winupdate tasks
tactical-celerywinupdate:
image: ${IMAGE_REPO}tactical:${VERSION}
command: ["tactical-celerywinupdate"]
restart: always
networks:
- redis
- proxy
- api-db
volumes:
- tactical_data:/opt/tactical
depends_on:
- tactical-postgres
- tactical-redis

View File

@@ -3,7 +3,7 @@
set -o errexit
set -o pipefail
DOCKER_IMAGES="tactical tactical-frontend tactical-nginx tactical-meshcentral tactical-salt tactical-nats"
DOCKER_IMAGES="tactical tactical-frontend tactical-nats tactical-nginx tactical-meshcentral"
cd ..

View File

@@ -8,14 +8,21 @@ temp="/tmp/tactical"
args="$*"
version="latest"
branch="master"
repo="wh1te909"
branchRegex=" --branch ([^ ]+)"
if [[ " ${args}" =~ ${branchRegex} ]]; then
branch="${BASH_REMATCH[1]}"
fi
repoRegex=" --repo ([^ ]+)"
if [[ " ${args}" =~ ${repoRegex} ]]; then
repo="${BASH_REMATCH[1]}"
fi
echo "repo=${repo}"
echo "branch=${branch}"
tactical_cli="https://raw.githubusercontent.com/wh1te909/tacticalrmm/${branch}/docker/tactical-cli"
tactical_cli="https://raw.githubusercontent.com/${repo}/tacticalrmm/${branch}/docker/tactical-cli"
versionRegex=" --version ([^ ]+)"
if [[ " ${args}" =~ ${versionRegex} ]]; then
@@ -36,7 +43,7 @@ if ! curl -sS "${tactical_cli}"; then
fi
chmod +x tactical-cli
./tactical-cli ${args} --version "${version}" 2>&1 | tee -a ~/install.log
tactical-cli ${args} --version "${version}" 2>&1 | tee -a ~/install.log
cd ~
if ! rm -rf "${temp}"; then

View File

@@ -18,7 +18,7 @@ sudo certbot certonly --manual -d *.example.com --agree-tos --no-bootstrap --man
## Configure DNS and firewall
You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443, 4222, 4505, 4506.
You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443 and 4222.
## Setting up the environment

View File

@@ -99,7 +99,7 @@ function generate_env {
echo "Generating env file in ${INSTALL_DIR}"
local config_file="$(cat << EOF
IMAGE_REPO=${REPO}
IMAGE_REPO=${DOCKER_REPO}
VERSION=${VERSION}
TRMM_USER=${USERNAME}
TRMM_PASS=${PASSWORD}
@@ -149,7 +149,8 @@ function initiate_letsencrypt {
FIRST_ARG="$1"
# defaults
REPO="tacticalrmm/"
DOCKER_REPO="tacticalrmm/"
REPO="wh1te909"
BRANCH="master"
VERSION="latest"
@@ -245,10 +246,21 @@ key="$1"
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
echo >&2 "--local option only valid for install and update. Exiting..."; exit 1;
REPO=""
DOCKER_REPO=""
shift # past argument
;;
# repo arg
--repo)
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
echo >&2 "--repo option only valid for install and update. Exiting..."; exit 1;
shift # past argument
REPO="$key"
shift # past value
;;
# branch arg
--branch)
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
@@ -358,8 +370,8 @@ if [[ "$MODE" == "install" ]]; then
cd "$INSTALL_DIR"
# pull docker-compose.yml file
echo "Downloading docker-compose.yml from branch ${branch}"
COMPOSE_FILE="https://raw.githubusercontent.com/wh1te909/tacticalrmm/${branch}/docker/docker-compose.yml"
echo "Downloading docker-compose.yml from branch ${BRANCH}"
COMPOSE_FILE="https://raw.githubusercontent.com/${REPO}/tacticalrmm/${BRANCH}/docker/docker-compose.yml"
if ! curl -sS "${COMPOSE_FILE}"; then
echo >&2 "Failed to download installation package ${COMPOSE_FILE}"
exit 1

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