Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01ee524049 | ||
|
|
af9cb65338 | ||
|
|
8aa11c580b | ||
|
|
ada627f444 | ||
|
|
a7b6d338c3 | ||
|
|
9f00538b97 | ||
|
|
a085015282 | ||
|
|
0b9c220fbb | ||
|
|
0e3d04873d | ||
|
|
b7578d939f | ||
|
|
b5c28de03f | ||
|
|
e17d25c156 | ||
|
|
c25dc1b99c | ||
|
|
a493a574bd | ||
|
|
4284493dce | ||
|
|
25059de8e1 | ||
|
|
1731b05ad0 | ||
|
|
e80dc663ac | ||
|
|
39988a4c2f | ||
|
|
415bff303a | ||
|
|
a65eb62a54 | ||
|
|
03b2982128 | ||
|
|
bff0527857 | ||
|
|
f3b7634254 | ||
|
|
6a9593c0b9 | ||
|
|
edb785b8e5 | ||
|
|
26d757b50a | ||
|
|
535079ee87 | ||
|
|
ac380c29c1 | ||
|
|
3fd212f26c | ||
|
|
04a3abc651 | ||
|
|
6caf85ddd1 | ||
|
|
16e4071508 | ||
|
|
69e7c4324b | ||
|
|
a1c4a8cbe5 | ||
|
|
e37f6cfda7 | ||
|
|
989c804409 | ||
|
|
7345bc3c82 | ||
|
|
69bee35700 | ||
|
|
598e24df7c | ||
|
|
0ae669201e | ||
|
|
f52a8a4642 | ||
|
|
9c40b61ef2 | ||
|
|
72dabcda83 | ||
|
|
161a06dbcc | ||
|
|
8ed3d4e70c | ||
|
|
a4223ccc8a | ||
|
|
ca85923855 | ||
|
|
52bfe7c493 | ||
|
|
4786bd0cbe | ||
|
|
cadab160ff | ||
|
|
6a7f17b2b0 | ||
|
|
4986a4d775 | ||
|
|
903af0c2cf | ||
|
|
3282fa803c | ||
|
|
67cc47608d | ||
|
|
0411704b8b | ||
|
|
1de85b2c69 | ||
|
|
33b012f29d | ||
|
|
1357584df3 | ||
|
|
e15809e271 | ||
|
|
0da1950427 | ||
|
|
e590b921be | ||
|
|
09462692f5 | ||
|
|
c1d1b5f762 | ||
|
|
6b9c87b858 | ||
|
|
485b6eb904 | ||
|
|
057630bdb5 | ||
|
|
6b02873b30 | ||
|
|
0fa0fc6d6b | ||
|
|
339ec07465 | ||
|
|
cd2e798fea | ||
|
|
d5cadbeae2 | ||
|
|
8046a3ccae | ||
|
|
bf91d60b31 | ||
|
|
539c047ec8 | ||
|
|
290c18fa87 | ||
|
|
98c46f5e57 | ||
|
|
f8bd5b5b4e | ||
|
|
816d32edad | ||
|
|
8453835c05 | ||
|
|
9328c356c8 | ||
|
|
89e3c1fc94 | ||
|
|
67e54cd15d | ||
|
|
278ea24786 | ||
|
|
aba1662631 | ||
|
|
61eeb60c19 | ||
|
|
5e9a8f4806 | ||
|
|
4cb274e9bc | ||
|
|
8b9b1a6a35 | ||
|
|
2655964113 | ||
|
|
188bad061b | ||
|
|
3af4c329aa | ||
|
|
6c13395f7d | ||
|
|
77b32ba360 | ||
|
|
91dba291ac | ||
|
|
a6bc293640 | ||
|
|
53882d6e5f | ||
|
|
d68adfbf10 | ||
|
|
498a392d7f | ||
|
|
740f6c05db | ||
|
|
d810ce301f | ||
|
|
5ef6a14d24 | ||
|
|
a13f6f1e68 | ||
|
|
d2d0f1aaee | ||
|
|
e64c72cc89 | ||
|
|
9ab915a08b | ||
|
|
e26fbf0328 | ||
|
|
d9a52c4a2a | ||
|
|
7b2ec90de9 | ||
|
|
d310bf8bbf | ||
|
|
2abc6cc939 | ||
|
|
56d4e694a2 | ||
|
|
5f002c9cdc | ||
|
|
759daf4b4a | ||
|
|
3a8d9568e3 | ||
|
|
ff22a9d94a | ||
|
|
a6e42d5374 | ||
|
|
a2f74e0488 | ||
|
|
ee44240569 | ||
|
|
d0828744a2 | ||
|
|
6e2e576b29 | ||
|
|
bf61e27f8a | ||
|
|
c441c30b46 | ||
|
|
0e741230ea | ||
|
|
1bfe9ac2db | ||
|
|
6812e72348 | ||
|
|
b6449d2f5b | ||
|
|
7e3ea20dce | ||
|
|
c9d6fe9dcd | ||
|
|
4a649a6b8b | ||
|
|
8fef184963 | ||
|
|
69583ca3c0 | ||
|
|
6038a68e91 | ||
|
|
fa8bd8db87 | ||
|
|
18b4f0ed0f | ||
|
|
461f9d66c9 | ||
|
|
2155103c7a | ||
|
|
c9a6839c45 | ||
|
|
9fbe331a80 | ||
|
|
a56389c4ce | ||
|
|
64656784cb | ||
|
|
6eff2c181e | ||
|
|
1aa48c6d62 | ||
|
|
c7ca1a346d | ||
|
|
fa0ec7b502 | ||
|
|
768438c136 | ||
|
|
9badea0b3c | ||
|
|
43263a1650 | ||
|
|
821e02dc75 | ||
|
|
ed011ecf28 | ||
|
|
d861de4c2f | ||
|
|
3a3b2449dc | ||
|
|
d2614406ca | ||
|
|
0798d098ae | ||
|
|
dab7ddc2bb | ||
|
|
081a96e281 | ||
|
|
a7dd881d79 | ||
|
|
8134d5e24d | ||
|
|
ba6756cd45 | ||
|
|
5d8fce21ac | ||
|
|
e7e4a5bcd4 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
10
.github/workflows/docker-build-push.yml
vendored
10
.github/workflows/docker-build-push.yml
vendored
@@ -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
19
.vscode/settings.json
vendored
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
[](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
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-14 01:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0009_user_show_community_scripts"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="agent_dblclick_action",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("editagent", "Edit Agent"),
|
||||
("takecontrol", "Take Control"),
|
||||
("remotebg", "Remote Background"),
|
||||
],
|
||||
default="editagent",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-18 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0010_user_agent_dblclick_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="default_agent_tbl_tab",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("server", "Servers"),
|
||||
("workstation", "Workstations"),
|
||||
("mixed", "Mixed"),
|
||||
],
|
||||
default="server",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Apiv2Config(AppConfig):
|
||||
name = "apiv2"
|
||||
@@ -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)
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from apiv3 import views as v3_views
|
||||
|
||||
urlpatterns = [
|
||||
path("newagent/", v3_views.NewAgent.as_view()),
|
||||
path("meshexe/", v3_views.MeshExe.as_view()),
|
||||
path("saltminion/", v3_views.SaltMinion.as_view()),
|
||||
path("<str:agentid>/saltminion/", v3_views.SaltMinion.as_view()),
|
||||
path("sysinfo/", v3_views.SysInfo.as_view()),
|
||||
path("hello/", v3_views.Hello.as_view()),
|
||||
path("checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
||||
]
|
||||
@@ -1,41 +0,0 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
|
||||
from checks.serializers import CheckRunnerGetSerializerV2
|
||||
|
||||
|
||||
class CheckRunner(APIView):
|
||||
"""
|
||||
For the windows python agent
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["last_seen"])
|
||||
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
|
||||
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_interval,
|
||||
"checks": CheckRunnerGetSerializerV2(checks, many=True).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request):
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
check.last_run = djangotime.now()
|
||||
check.save(update_fields=["last_run"])
|
||||
status = check.handle_checkv2(request.data)
|
||||
return Response(status)
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
30
api/tacticalrmm/checks/migrations/0011_check_run_history.py
Normal file
30
api/tacticalrmm/checks/migrations/0011_check_run_history.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
39
api/tacticalrmm/checks/migrations/0011_checkhistory.py
Normal file
39
api/tacticalrmm/checks/migrations/0011_checkhistory.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/checks/migrations/0012_auto_20210110_0503.py
Normal file
18
api/tacticalrmm/checks/migrations/0012_auto_20210110_0503.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/checks/migrations/0013_auto_20210110_0505.py
Normal file
18
api/tacticalrmm/checks/migrations/0013_auto_20210110_0505.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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 = []
|
||||
27
api/tacticalrmm/checks/migrations/0015_auto_20210110_1808.py
Normal file
27
api/tacticalrmm/checks/migrations/0015_auto_20210110_1808.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}'",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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="")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
5
api/tacticalrmm/natsapi/apps.py
Normal file
5
api/tacticalrmm/natsapi/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NatsapiConfig(AppConfig):
|
||||
name = "natsapi"
|
||||
23
api/tacticalrmm/natsapi/tests.py
Normal file
23
api/tacticalrmm/natsapi/tests.py
Normal 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)
|
||||
13
api/tacticalrmm/natsapi/urls.py
Normal file
13
api/tacticalrmm/natsapi/urls.py
Normal 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()),
|
||||
]
|
||||
286
api/tacticalrmm/natsapi/views.py
Normal file
286
api/tacticalrmm/natsapi/views.py
Normal 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")
|
||||
@@ -1,3 +1,6 @@
|
||||
black
|
||||
Werkzeug
|
||||
django-extensions
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
pymdown-extensions
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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']}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -14,8 +14,8 @@ def get_debug_info():
|
||||
|
||||
|
||||
EXCLUDE_PATHS = (
|
||||
"/natsapi",
|
||||
"/api/v3",
|
||||
"/api/v2",
|
||||
"/logs/auditlogs",
|
||||
f"/{settings.ADMIN_URL}",
|
||||
"/logout",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" ]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ..
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user