Compare commits

..

1 Commits

Author SHA1 Message Date
wh1te909
3588bf827b Release 0.11.0 2022-01-13 01:31:45 +00:00
792 changed files with 75946 additions and 10663 deletions

View File

@@ -23,7 +23,7 @@ POSTGRES_USER=postgres
POSTGRES_PASS=postgrespass POSTGRES_PASS=postgrespass
# DEV SETTINGS # DEV SETTINGS
APP_PORT=443 APP_PORT=80
API_PORT=80 API_PORT=80
HTTP_PROTOCOL=https HTTP_PROTOCOL=https
DOCKER_NETWORK=172.21.0.0/24 DOCKER_NETWORK=172.21.0.0/24

View File

@@ -1,11 +1,4 @@
# pulls community scripts from git repo FROM python:3.9.9-slim
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
RUN apt-get update && \
apt-get install -y --no-install-recommends git && \
git clone https://github.com/amidaware/community-scripts.git /community-scripts
FROM python:3.10-slim
ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
@@ -17,15 +10,9 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000 8383 8005 EXPOSE 8000 8383 8005
RUN apt-get update && \
apt-get install -y build-essential
RUN groupadd -g 1000 tactical && \ RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical useradd -u 1000 -g 1000 tactical
# copy community scripts
COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts
# Copy dev python reqs # Copy dev python reqs
COPY .devcontainer/requirements.txt / COPY .devcontainer/requirements.txt /

View File

@@ -0,0 +1,19 @@
version: '3.4'
services:
api-dev:
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"]
ports:
- 8000:8000
- 5678:5678
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
networks:
dev:
aliases:
- tactical-backend

View File

@@ -5,7 +5,6 @@ services:
container_name: trmm-api-dev container_name: trmm-api-dev
image: api-dev image: api-dev
restart: always restart: always
user: 1000:1000
build: build:
context: .. context: ..
dockerfile: .devcontainer/api.dockerfile dockerfile: .devcontainer/api.dockerfile
@@ -24,10 +23,10 @@ services:
app-dev: app-dev:
container_name: trmm-app-dev container_name: trmm-app-dev
image: node:16-alpine image: node:14-alpine
restart: always restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve" command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve
user: 1000:1000 -- --host 0.0.0.0 --port ${APP_PORT}"
working_dir: /workspace/web working_dir: /workspace/web
volumes: volumes:
- ..:/workspace:cached - ..:/workspace:cached
@@ -43,7 +42,6 @@ services:
container_name: trmm-nats-dev container_name: trmm-nats-dev
image: ${IMAGE_REPO}tactical-nats:${VERSION} image: ${IMAGE_REPO}tactical-nats:${VERSION}
restart: always restart: always
user: 1000:1000
environment: environment:
API_HOST: ${API_HOST} API_HOST: ${API_HOST}
API_PORT: ${API_PORT} API_PORT: ${API_PORT}
@@ -64,7 +62,6 @@ services:
container_name: trmm-meshcentral-dev container_name: trmm-meshcentral-dev
image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
restart: always restart: always
user: 1000:1000
environment: environment:
MESH_HOST: ${MESH_HOST} MESH_HOST: ${MESH_HOST}
MESH_USER: ${MESH_USER} MESH_USER: ${MESH_USER}
@@ -88,7 +85,6 @@ services:
container_name: trmm-mongodb-dev container_name: trmm-mongodb-dev
image: mongo:4.4 image: mongo:4.4
restart: always restart: always
user: 1000:1000
environment: environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER} MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
@@ -106,7 +102,7 @@ services:
image: postgres:13-alpine image: postgres:13-alpine
restart: always restart: always
environment: environment:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: tacticalrmm
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASS} POSTGRES_PASSWORD: ${POSTGRES_PASS}
volumes: volumes:
@@ -120,8 +116,7 @@ services:
redis-dev: redis-dev:
container_name: trmm-redis-dev container_name: trmm-redis-dev
restart: always restart: always
user: 1000:1000 command: redis-server --appendonly yes
command: redis-server
image: redis:6.0-alpine image: redis:6.0-alpine
volumes: volumes:
- redis-data-dev:/data - redis-data-dev:/data
@@ -146,7 +141,6 @@ services:
TRMM_PASS: ${TRMM_PASS} TRMM_PASS: ${TRMM_PASS}
HTTP_PROTOCOL: ${HTTP_PROTOCOL} HTTP_PROTOCOL: ${HTTP_PROTOCOL}
APP_PORT: ${APP_PORT} APP_PORT: ${APP_PORT}
POSTGRES_DB: ${POSTGRES_DB}
depends_on: depends_on:
- postgres-dev - postgres-dev
- meshcentral-dev - meshcentral-dev
@@ -154,9 +148,6 @@ services:
- dev - dev
volumes: volumes:
- tactical-data-dev:/opt/tactical - tactical-data-dev:/opt/tactical
- mesh-data-dev:/meshcentral-data
- redis-data-dev:/redis/data
- mongo-dev-data:/mongo/data/db
- ..:/workspace:cached - ..:/workspace:cached
# container for celery worker service # container for celery worker service
@@ -165,7 +156,6 @@ services:
image: api-dev image: api-dev
command: [ "tactical-celery-dev" ] command: [ "tactical-celery-dev" ]
restart: always restart: always
user: 1000:1000
networks: networks:
- dev - dev
volumes: volumes:
@@ -181,7 +171,6 @@ services:
image: api-dev image: api-dev
command: [ "tactical-celerybeat-dev" ] command: [ "tactical-celerybeat-dev" ]
restart: always restart: always
user: 1000:1000
networks: networks:
- dev - dev
volumes: volumes:
@@ -197,7 +186,6 @@ services:
image: api-dev image: api-dev
command: [ "tactical-websockets-dev" ] command: [ "tactical-websockets-dev" ]
restart: always restart: always
user: 1000:1000
networks: networks:
dev: dev:
aliases: aliases:
@@ -214,7 +202,6 @@ services:
container_name: trmm-nginx-dev container_name: trmm-nginx-dev
image: ${IMAGE_REPO}tactical-nginx:${VERSION} image: ${IMAGE_REPO}tactical-nginx:${VERSION}
restart: always restart: always
user: 1000:1000
environment: environment:
APP_HOST: ${APP_HOST} APP_HOST: ${APP_HOST}
API_HOST: ${API_HOST} API_HOST: ${API_HOST}
@@ -228,11 +215,23 @@ services:
dev: dev:
ipv4_address: ${DOCKER_NGINX_IP} ipv4_address: ${DOCKER_NGINX_IP}
ports: ports:
- "80:8080" - "80:80"
- "443:4443" - "443:443"
volumes: volumes:
- tactical-data-dev:/opt/tactical - tactical-data-dev:/opt/tactical
mkdocs-dev:
container_name: trmm-mkdocs-dev
image: api-dev
restart: always
command: [ "tactical-mkdocs-dev" ]
ports:
- "8005:8005"
volumes:
- ..:/workspace:cached
networks:
- dev
volumes: volumes:
tactical-data-dev: null tactical-data-dev: null
postgres-data-dev: null postgres-data-dev: null

View File

@@ -10,7 +10,7 @@ set -e
: "${POSTGRES_PASS:=tactical}" : "${POSTGRES_PASS:=tactical}"
: "${POSTGRES_DB:=tacticalrmm}" : "${POSTGRES_DB:=tacticalrmm}"
: "${MESH_SERVICE:=tactical-meshcentral}" : "${MESH_SERVICE:=tactical-meshcentral}"
: "${MESH_WS_URL:=ws://${MESH_SERVICE}:4443}" : "${MESH_WS_URL:=ws://${MESH_SERVICE}:443}"
: "${MESH_USER:=meshcentral}" : "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}" : "${MESH_PASS:=meshcentralpass}"
: "${MESH_HOST:=tactical-meshcentral}" : "${MESH_HOST:=tactical-meshcentral}"
@@ -41,7 +41,7 @@ function django_setup {
sleep 5 sleep 5
done done
until (echo > /dev/tcp/"${MESH_SERVICE}"/4443) &> /dev/null; do until (echo > /dev/tcp/"${MESH_SERVICE}"/443) &> /dev/null; do
echo "waiting for meshcentral container to be ready..." echo "waiting for meshcentral container to be ready..."
sleep 5 sleep 5
done done
@@ -60,12 +60,10 @@ DEBUG = True
DOCKER_BUILD = True DOCKER_BUILD = True
SWAGGER_ENABLED = True
CERT_FILE = '${CERT_PUB_PATH}' CERT_FILE = '${CERT_PUB_PATH}'
KEY_FILE = '${CERT_PRIV_PATH}' KEY_FILE = '${CERT_PRIV_PATH}'
SCRIPTS_DIR = '/community-scripts' SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts'
ALLOWED_HOSTS = ['${API_HOST}', '*'] ALLOWED_HOSTS = ['${API_HOST}', '*']
@@ -96,7 +94,6 @@ EOF
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
# run migrations and init scripts # run migrations and init scripts
"${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
"${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input "${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
"${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup "${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
@@ -120,24 +117,8 @@ if [ "$1" = 'tactical-init-dev' ]; then
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}" test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
mkdir -p /meshcentral-data
mkdir -p ${TACTICAL_DIR}/tmp
mkdir -p ${TACTICAL_DIR}/certs
mkdir -p /mongo/data/db
mkdir -p /redis/data
touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data
touch ${TACTICAL_DIR}/tmp/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}
touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs
touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db
touch /redis/data/.initialized && chown -R 1000:1000 /redis/data
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log
touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log
# setup Python virtual env and install dependencies # setup Python virtual env and install dependencies
! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV} ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
"${VIRTUAL_ENV}"/bin/python -m pip install --upgrade pip
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir setuptools wheel
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
django_setup django_setup
@@ -146,7 +127,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
webenv="$(cat << EOF webenv="$(cat << EOF
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}" PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}" DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_PORT = ${APP_PORT} APP_URL = "https://${APP_HOST}"
DOCKER_BUILD = 1 DOCKER_BUILD = 1
EOF EOF
)" )"
@@ -180,3 +161,8 @@ if [ "$1" = 'tactical-websockets-dev' ]; then
check_tactical_ready check_tactical_ready
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0 "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
fi fi
if [ "$1" = 'tactical-mkdocs-dev' ]; then
cd "${WORKSPACE_DIR}/docs"
"${VIRTUAL_ENV}"/bin/mkdocs serve
fi

View File

@@ -1,41 +1,39 @@
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
asgiref==3.5.0 asyncio-nats-client
celery==5.2.6 celery
channels==3.0.4 channels
channels_redis==3.4.0 channels_redis
daphne==3.0.2 django-ipware
Django==4.0.4 Django==3.2.10
django-cors-headers==3.11.0 django-cors-headers
django-ipware==4.0.2 django-rest-knox
django-rest-knox==4.2.0 djangorestframework
djangorestframework==3.13.1 loguru
future==0.18.2 msgpack
msgpack==1.0.3 psycopg2-binary
nats-py==2.1.0 pycparser
packaging==21.3 pycryptodome
psycopg2-binary==2.9.3 pyotp
pycryptodome==3.14.1 pyparsing
pyotp==2.6.0 pytz
pytz==2022.1 qrcode
qrcode==7.3.1 redis
redis==4.2.2 twilio
requests==2.27.1 packaging
twilio==7.8.1 validators
urllib3==1.26.9 websockets
validators==0.18.2 black
websockets==10.2 Werkzeug
drf_spectacular==0.22.0 django-extensions
meshctrl==0.1.15 coverage
hiredis==2.0.0 coveralls
model_bakery
# dev mkdocs
black==22.3.0 mkdocs-material
django-extensions==3.1.5 pymdown-extensions
isort==5.10.1 Pygments
mypy==0.942 mypy
types-pytz==2021.3.6 pysnooper
model-bakery==1.5.0 isort
coverage==6.3.2 drf_spectacular
django-silk==4.3.0 pandas
django-stubs==1.10.1
djangorestframework-stubs==1.5.0

View File

@@ -1,73 +0,0 @@
name: Tests CI
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
jobs:
test:
runs-on: ubuntu-latest
name: Tests
strategy:
matrix:
python-version: ['3.10.4']
steps:
- uses: actions/checkout@v3
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '14'
postgresql db: 'pipeline'
postgresql user: 'pipeline'
postgresql password: 'pipeline123456'
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install redis
run: |
sudo apt update
sudo apt install -y redis
redis-server --version
- name: Install requirements
working-directory: api/tacticalrmm
run: |
python --version
SETTINGS_FILE="tacticalrmm/settings.py"
SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
pip install --upgrade pip
pip install setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
pip install -r requirements.txt -r requirements-test.txt
- name: Codestyle black
working-directory: api/tacticalrmm
run: |
black --exclude migrations/ --check tacticalrmm
if [ $? -ne 0 ]; then
exit 1
fi
- name: Run django tests
env:
GHACTIONS: "yes"
working-directory: api/tacticalrmm
run: |
pytest
if [ $? -ne 0 ]; then
exit 1
fi
- uses: codecov/codecov-action@v3
with:
directory: ./api/tacticalrmm
files: ./api/tacticalrmm/coverage.xml
verbose: true

View File

@@ -32,7 +32,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'go', 'python' ] language: [ 'go', 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Learn more about CodeQL language support at https://git.io/codeql-language-support

22
.github/workflows/deploy-docs.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Deploy Docs
on:
push:
branches:
- master
defaults:
run:
working-directory: docs
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install --upgrade pip
- run: pip install --upgrade setuptools wheel
- run: pip install mkdocs mkdocs-material pymdown-extensions
- run: mkdocs gh-deploy --force

6
.gitignore vendored
View File

@@ -50,8 +50,4 @@ docs/site/
reset_db.sh reset_db.sh
run_go_cmd.py run_go_cmd.py
nats-api.conf nats-api.conf
ignore/
coverage.lcov
daphne.sock.lock
.pytest_cache
coverage.xml

View File

@@ -1,23 +0,0 @@
{
"recommendations": [
// frontend
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight",
// python
"matangover.mypy",
"ms-python.python",
// golang
"golang.go"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

53
.vscode/settings.json vendored
View File

@@ -1,32 +1,31 @@
{ {
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python", "python.pythonPath": "api/tacticalrmm/env/bin/python",
"python.languageServer": "Pylance", "python.languageServer": "Pylance",
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"], "python.analysis.extraPaths": [
"api/tacticalrmm",
"api/env",
],
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error", "reportUnusedImport": "error",
"reportDuplicateImport": "error", "reportDuplicateImport": "error",
"reportGeneralTypeIssues": "none"
}, },
"python.analysis.typeCheckingMode": "basic", "python.analysis.memory.keepLibraryAst": true,
"python.linting.enabled": true,
"python.linting.mypyEnabled": true, "python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [ "python.analysis.typeCheckingMode": "basic",
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.linting.ignorePatterns": [
"**/site-packages/**/*.py",
".vscode/*.py",
"**env/**"
],
"python.formatting.provider": "black", "python.formatting.provider": "black",
"mypy.targets": ["api/tacticalrmm"],
"mypy.runUsingActiveInterpreter": true,
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
}
},
"vetur.format.options.tabSize": 2,
"vetur.format.options.useTabs": false,
"files.watcherExclude": { "files.watcherExclude": {
"files.watcherExclude": { "files.watcherExclude": {
"**/.git/objects/**": true, "**/.git/objects/**": true,
@@ -47,23 +46,25 @@
"**/*.parquet*": true, "**/*.parquet*": true,
"**/*.pyc": true, "**/*.pyc": true,
"**/*.zip": true "**/*.zip": true
} },
}, },
"go.useLanguageServer": true, "go.useLanguageServer": true,
"[go]": { "[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": false "source.organizeImports": false,
}, },
"editor.snippetSuggestions": "none" "editor.snippetSuggestions": "none",
}, },
"[go.mod]": { "[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": true,
} },
}, },
"gopls": { "gopls": {
"usePlaceholders": true, "usePlaceholders": true,
"completeUnimported": true, "completeUnimported": true,
"staticcheck": true "staticcheck": true,
} }
} }

23
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "docker debug",
"type": "shell",
"command": "docker-compose",
"args": [
"-p",
"trmm",
"-f",
".devcontainer/docker-compose.yml",
"-f",
".devcontainer/docker-compose.debug.yml",
"up",
"-d",
"--build"
]
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present wh1te909
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,74 +0,0 @@
### Tactical RMM License Version 1.0
Text of license:&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC. All rights reserved.<br>
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;Amending the text of this license is not permitted.
Trade Mark:&emsp;&emsp;&emsp;&emsp;"Tactical RMM" is a trade mark of AmidaWare LLC.
Licensor:&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;&nbsp;AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA.
Licensed Software:&emsp;&nbsp;The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions).
### 1. Preamble
The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes.
The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor.
### 2. License Grant
Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license.
* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted.
This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following:
* a service allowing third parties to interact remotely through a computer network;
* as part of a SaaS service or product;
* as part of the provision of a managed hosting service or product;
* the offering of installation and/or configuration services;
* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM).
The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work.
The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works.
All use of the Licensed Software must immediately cease if use breaches the terms of this license.
### 3. Derivative Works
If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online.
### 4. Copyright Notice
The following copyright notice shall be included in all copies of the Licensed Software:
&emsp;&emsp;&emsp;Copyright © 2022 AmidaWare LLC.
&emsp;&emsp;&emsp;Licensed under the Tactical RMM License Version 1.0 (the “License”).<br>
&emsp;&emsp;&emsp;You may only use the Licensed Software in accordance with the License.<br>
&emsp;&emsp;&emsp;A copy of the License is available at: https://license.tacticalrmm.com
### 5. Disclaimer of Warranty
THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT.
### 6. Limits of Liability
THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA.
### 7. Termination
This license shall terminate with immediate effect if there is a material breach of any of its terms.
### 8. No partnership, agency or joint venture
Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person.
### 9. No endorsement
The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent.
### 10. Trademarks
No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice).
### 11. Entire agreement
This license contains the whole agreement relating to its subject matter.
### 12. Severance
If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license.
### 13. Acceptance of these terms
The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software.

View File

@@ -1,18 +1,19 @@
# Tactical RMM # Tactical RMM
![CI Tests](https://github.com/amidaware/tacticalrmm/actions/workflows/ci-tests.yml/badge.svg?branch=develop) [![Build Status](https://dev.azure.com/dcparsi/Tactical%20RMM/_apis/build/status/wh1te909.tacticalrmm?branchName=develop)](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
[![codecov](https://codecov.io/gh/amidaware/tacticalrmm/branch/develop/graph/badge.svg?token=8ACUPVPTH6)](https://codecov.io/gh/amidaware/tacticalrmm) [![Coverage Status](https://coveralls.io/repos/github/wh1te909/tacticalrmm/badge.png?branch=develop&kill_cache=1)](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\ Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
It uses an [agent](https://github.com/amidaware/rmmagent) written in golang and integrates with [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://demo.tacticalrmm.com/) # [LIVE DEMO](https://rmm.tacticalrmm.io/)
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app. Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
### [Discord Chat](https://discord.gg/upGTkWp) ### [Discord Chat](https://discord.gg/upGTkWp)
### [Documentation](https://docs.tacticalrmm.com) ### [Documentation](https://wh1te909.github.io/tacticalrmm/)
## Features ## Features
@@ -28,13 +29,10 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
- Remote software installation via chocolatey - Remote software installation via chocolatey
- Software and hardware inventory - Software and hardware inventory
## Windows agent versions supported ## Windows versions supported
- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022 - Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
## Linux agent versions supported
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
## Installation / Backup / Restore / Usage ## Installation / Backup / Restore / Usage
### Refer to the [documentation](https://docs.tacticalrmm.com) ### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)

View File

@@ -2,11 +2,18 @@
## Supported Versions ## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 0.12.2 | :white_check_mark: | | 0.10.4 | :white_check_mark: |
| < 0.12.2 | :x: | | < 0.10.4| :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability
https://docs.tacticalrmm.com/security Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

View File

@@ -1,15 +1,26 @@
[run] [run]
include = *.py source = .
omit =
tacticalrmm/asgi.py
tacticalrmm/wsgi.py
manage.py
*/__pycache__/*
*/env/*
*/baker_recipes.py
/usr/local/lib/*
**/migrations/*
**/test*.py
[report] [report]
show_missing = True show_missing = True
include = *.py
omit =
*/__pycache__/*
*/env/*
*/management/*
*/migrations/*
*/static/*
manage.py
*/local_settings.py
*/apps.py
*/admin.py
*/celery.py
*/wsgi.py
*/settings.py
*/baker_recipes.py
*/urls.py
*/tests.py
*/test.py
checks/utils.py
*/asgi.py
*/demo_views.py

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from rest_framework.authtoken.admin import TokenAdmin from rest_framework.authtoken.admin import TokenAdmin
from .models import Role, User from .models import User, Role
admin.site.register(User) admin.site.register(User)
TokenAdmin.raw_id_fields = ("user",) TokenAdmin.raw_id_fields = ("user",)

View File

@@ -1,23 +1,19 @@
import uuid import uuid
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from accounts.models import User from accounts.models import User
class Command(BaseCommand): class Command(BaseCommand):
help = "Creates the installer user" help = "Creates the installer user"
def handle(self, *args, **kwargs): # type: ignore def handle(self, *args, **kwargs):
self.stdout.write("Checking if installer user has been created...")
if User.objects.filter(is_installer_user=True).exists(): if User.objects.filter(is_installer_user=True).exists():
self.stdout.write("Installer user already exists")
return return
User.objects.create_user( User.objects.create_user( # type: ignore
username=uuid.uuid4().hex, username=uuid.uuid4().hex,
is_installer_user=True, is_installer_user=True,
password=User.objects.make_random_password(60), password=User.objects.make_random_password(60), # type: ignore
block_dashboard_login=True, block_dashboard_login=True,
) )
self.stdout.write("Installer user has been created")

View File

@@ -6,7 +6,7 @@ from knox.models import AuthToken
class Command(BaseCommand): class Command(BaseCommand):
help = "Deletes all knox web tokens" help = "Deletes all knox web tokens"
def handle(self, *args, **kwargs): # type: ignore def handle(self, *args, **kwargs):
# only delete web tokens, not any generated by the installer or deployments # only delete web tokens, not any generated by the installer or deployments
dont_delete = djangotime.now() + djangotime.timedelta(hours=23) dont_delete = djangotime.now() + djangotime.timedelta(hours=23)
tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter( tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter(

View File

@@ -1,5 +1,4 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from accounts.models import User from accounts.models import User

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.1 on 2021-05-11 02:33 # Generated by Django 3.2.1 on 2021-05-11 02:33
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.6 on 2021-09-03 00:54 # Generated by Django 3.2.6 on 2021-09-03 00:54
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.6 on 2021-10-10 02:49 # Generated by Django 3.2.6 on 2021-10-10 02:49
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-02 15:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0030_auto_20211104_0221'),
]
operations = [
migrations.AddField(
model_name='user',
name='date_format',
field=models.CharField(blank=True, max_length=30, null=True),
),
]

View File

@@ -1,17 +1,26 @@
from typing import Optional
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models.fields import CharField, DateTimeField from django.db.models.fields import CharField, DateTimeField
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
from tacticalrmm.constants import (
ROLE_CACHE_PREFIX, AGENT_DBLCLICK_CHOICES = [
AgentDblClick, ("editagent", "Edit Agent"),
AgentTableTabs, ("takecontrol", "Take Control"),
ClientTreeSort, ("remotebg", "Remote Background"),
) ("urlaction", "URL Action"),
]
AGENT_TBL_TAB_CHOICES = [
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
]
CLIENT_TREE_SORT_CHOICES = [
("alphafail", "Move failing clients to the top"),
("alpha", "Sort alphabetically"),
]
class User(AbstractUser, BaseAuditModel): class User(AbstractUser, BaseAuditModel):
@@ -20,8 +29,8 @@ class User(AbstractUser, BaseAuditModel):
totp_key = models.CharField(max_length=50, null=True, blank=True) totp_key = models.CharField(max_length=50, null=True, blank=True)
dark_mode = models.BooleanField(default=True) dark_mode = models.BooleanField(default=True)
show_community_scripts = models.BooleanField(default=True) show_community_scripts = models.BooleanField(default=True)
agent_dblclick_action: "AgentDblClick" = models.CharField( agent_dblclick_action = models.CharField(
max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
) )
url_action = models.ForeignKey( url_action = models.ForeignKey(
"core.URLAction", "core.URLAction",
@@ -31,16 +40,15 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
default_agent_tbl_tab = models.CharField( default_agent_tbl_tab = models.CharField(
max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.SERVER max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
) )
agents_per_page = models.PositiveIntegerField(default=50) # not currently used agents_per_page = models.PositiveIntegerField(default=50) # not currently used
client_tree_sort = models.CharField( client_tree_sort = models.CharField(
max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
) )
client_tree_splitter = models.PositiveIntegerField(default=11) client_tree_splitter = models.PositiveIntegerField(default=11)
loading_bar_color = models.CharField(max_length=255, default="red") loading_bar_color = models.CharField(max_length=255, default="red")
clear_search_when_switching = models.BooleanField(default=True) clear_search_when_switching = models.BooleanField(default=True)
date_format = models.CharField(max_length=30, blank=True, null=True)
is_installer_user = models.BooleanField(default=False) is_installer_user = models.BooleanField(default=False)
last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True) last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True)
@@ -67,23 +75,6 @@ class User(AbstractUser, BaseAuditModel):
return UserSerializer(user).data return UserSerializer(user).data
def get_and_set_role_cache(self) -> "Optional[Role]":
role = cache.get(f"{ROLE_CACHE_PREFIX}{self.role}")
if role and isinstance(role, Role):
return role
elif not role and not self.role:
return None
else:
models.prefetch_related_objects(
[self.role],
"can_view_clients",
"can_view_sites",
)
cache.set(f"{ROLE_CACHE_PREFIX}{self.role}", self.role, 600)
return self.role
class Role(BaseAuditModel): class Role(BaseAuditModel):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@@ -184,12 +175,6 @@ class Role(BaseAuditModel):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs) -> None:
# delete cache on save
cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
super(BaseAuditModel, self).save(*args, **kwargs)
@staticmethod @staticmethod
def serialize(role): def serialize(role):
# serializes the agent and returns json # serializes the agent and returns json

View File

@@ -4,7 +4,7 @@ from tacticalrmm.permissions import _has_perm
class AccountsPerms(permissions.BasePermission): class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_accounts") return _has_perm(r, "can_list_accounts")
else: else:
@@ -28,7 +28,7 @@ class AccountsPerms(permissions.BasePermission):
class RolesPerms(permissions.BasePermission): class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_roles") return _has_perm(r, "can_list_roles")
else: else:
@@ -36,7 +36,7 @@ class RolesPerms(permissions.BasePermission):
class APIKeyPerms(permissions.BasePermission): class APIKeyPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_api_keys") return _has_perm(r, "can_list_api_keys")

View File

@@ -1,11 +1,11 @@
import pyotp import pyotp
from rest_framework.serializers import ( from rest_framework.serializers import (
ModelSerializer, ModelSerializer,
ReadOnlyField,
SerializerMethodField, SerializerMethodField,
ReadOnlyField,
) )
from .models import APIKey, Role, User from .models import APIKey, User, Role
class UserUISerializer(ModelSerializer): class UserUISerializer(ModelSerializer):
@@ -22,7 +22,6 @@ class UserUISerializer(ModelSerializer):
"loading_bar_color", "loading_bar_color",
"clear_search_when_switching", "clear_search_when_switching",
"block_dashboard_login", "block_dashboard_login",
"date_format",
] ]
@@ -40,7 +39,6 @@ class UserSerializer(ModelSerializer):
"last_login_ip", "last_login_ip",
"role", "role",
"block_dashboard_login", "block_dashboard_login",
"date_format",
] ]

View File

@@ -2,16 +2,15 @@ from unittest.mock import patch
from django.test import override_settings from django.test import override_settings
from model_bakery import baker, seq from model_bakery import baker, seq
from accounts.models import User, APIKey
from accounts.models import APIKey, User
from accounts.serializers import APIKeySerializer
from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from accounts.serializers import APIKeySerializer
class TestAccounts(TacticalTestCase): class TestAccounts(TacticalTestCase):
def setUp(self): def setUp(self):
self.setup_client() self.client_setup()
self.bob = User(username="bob") self.bob = User(username="bob")
self.bob.set_password("hunter2") self.bob.set_password("hunter2")
self.bob.save() self.bob.save()
@@ -70,17 +69,17 @@ class TestAccounts(TacticalTestCase):
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
self.assertIn("non_field_errors", r.data.keys()) self.assertIn("non_field_errors", r.data.keys())
# @override_settings(DEBUG=True) @override_settings(DEBUG=True)
# @patch("pyotp.TOTP.verify") @patch("pyotp.TOTP.verify")
# def test_debug_login_view(self, mock_verify): def test_debug_login_view(self, mock_verify):
# url = "/login/" url = "/login/"
# mock_verify.return_value = True mock_verify.return_value = True
# data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"} data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"}
# r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
# self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
# self.assertIn("expiry", r.data.keys()) self.assertIn("expiry", r.data.keys())
# self.assertIn("token", r.data.keys()) self.assertIn("token", r.data.keys())
class TestGetAddUsers(TacticalTestCase): class TestGetAddUsers(TacticalTestCase):
@@ -284,9 +283,9 @@ class TestUserAction(TacticalTestCase):
data = { data = {
"dark_mode": True, "dark_mode": True,
"show_community_scripts": True, "show_community_scripts": True,
"agent_dblclick_action": AgentDblClick.EDIT_AGENT, "agent_dblclick_action": "editagent",
"default_agent_tbl_tab": AgentTableTabs.MIXED, "default_agent_tbl_tab": "mixed",
"client_tree_sort": ClientTreeSort.ALPHA, "client_tree_sort": "alpha",
"client_tree_splitter": 14, "client_tree_splitter": 14,
"loading_bar_color": "green", "loading_bar_color": "green",
"clear_search_when_switching": False, "clear_search_when_switching": False,
@@ -309,7 +308,7 @@ class TestAPIKeyViews(TacticalTestCase):
serializer = APIKeySerializer(apikeys, many=True) serializer = APIKeySerializer(apikeys, many=True)
resp = self.client.get(url, format="json") resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(serializer.data, resp.data) self.assertEqual(serializer.data, resp.data) # type: ignore
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@@ -332,14 +331,14 @@ class TestAPIKeyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
apikey = baker.make("accounts.APIKey", name="Test") apikey = baker.make("accounts.APIKey", name="Test")
url = f"/accounts/apikeys/{apikey.pk}/" url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
data = {"name": "New Name"} data = {"name": "New Name"} # type: ignore
resp = self.client.put(url, data, format="json") resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
apikey = APIKey.objects.get(pk=apikey.pk) apikey = APIKey.objects.get(pk=apikey.pk) # type: ignore
self.assertEqual(apikey.name, "New Name") self.assertEquals(apikey.name, "New Name")
self.check_not_authenticated("put", url) self.check_not_authenticated("put", url)
@@ -350,11 +349,11 @@ class TestAPIKeyViews(TacticalTestCase):
# test delete api key # test delete api key
apikey = baker.make("accounts.APIKey") apikey = baker.make("accounts.APIKey")
url = f"/accounts/apikeys/{apikey.pk}/" url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
resp = self.client.delete(url, format="json") resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) # type: ignore
self.check_not_authenticated("delete", url) self.check_not_authenticated("delete", url)
@@ -394,7 +393,7 @@ class TestAPIAuthentication(TacticalTestCase):
name="Test Token", key="123456", user=self.user name="Test Token", key="123456", user=self.user
) )
self.setup_client() self.client_setup()
def test_api_auth(self): def test_api_auth(self):
url = "/clients/" url = "/clients/"

View File

@@ -5,16 +5,15 @@ from django.db import IntegrityError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ipware import get_client_ip from ipware import get_client_ip
from knox.views import LoginView as KnoxLoginView from knox.views import LoginView as KnoxLoginView
from logs.models import AuditLog
from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from logs.models import AuditLog
from tacticalrmm.helpers import notify_error
from .models import APIKey, Role, User from .models import APIKey, Role, User
from .permissions import AccountsPerms, APIKeyPerms, RolesPerms from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
from .serializers import ( from .serializers import (
APIKeySerializer, APIKeySerializer,
RoleSerializer, RoleSerializer,

View File

@@ -1,8 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import Agent, AgentCustomField, AgentHistory, Note from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
admin.site.register(Agent) admin.site.register(Agent)
admin.site.register(RecoveryAction)
admin.site.register(Note) admin.site.register(Note)
admin.site.register(AgentCustomField) admin.site.register(AgentCustomField)
admin.site.register(AgentHistory) admin.site.register(AgentHistory)

View File

@@ -1,6 +1,6 @@
import json import json
import os import os
import secrets import random
import string import string
from itertools import cycle from itertools import cycle
@@ -8,11 +8,10 @@ from django.conf import settings
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from model_bakery.recipe import Recipe, foreign_key, seq from model_bakery.recipe import Recipe, foreign_key, seq
from tacticalrmm.constants import AgentMonType, AgentPlat
def generate_agent_id(hostname):
def generate_agent_id() -> str: rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
return "".join(secrets.choice(string.ascii_letters) for i in range(39)) return f"{rand}-{hostname}"
site = Recipe("clients.Site") site = Recipe("clients.Site")
@@ -25,34 +24,25 @@ def get_wmi_data():
return json.load(f) return json.load(f)
def get_win_svcs():
svcs = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
with open(svcs) as f:
return json.load(f)
agent = Recipe( agent = Recipe(
"agents.Agent", "agents.Agent",
site=foreign_key(site), site=foreign_key(site),
hostname="DESKTOP-TEST123", hostname="DESKTOP-TEST123",
version="1.3.0", version="1.3.0",
monitoring_type=cycle(AgentMonType.values), monitoring_type=cycle(["workstation", "server"]),
agent_id=seq(generate_agent_id()), agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
last_seen=djangotime.now() - djangotime.timedelta(days=5), last_seen=djangotime.now() - djangotime.timedelta(days=5),
plat=AgentPlat.WINDOWS,
) )
server_agent = agent.extend( server_agent = agent.extend(
monitoring_type=AgentMonType.SERVER, monitoring_type="server",
) )
workstation_agent = agent.extend( workstation_agent = agent.extend(
monitoring_type=AgentMonType.WORKSTATION, monitoring_type="workstation",
) )
online_agent = agent.extend( online_agent = agent.extend(last_seen=djangotime.now())
last_seen=djangotime.now(), services=get_win_svcs(), wmi_detail=get_wmi_data()
)
offline_agent = agent.extend( offline_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=7) last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
@@ -87,4 +77,4 @@ agent_with_services = agent.extend(
], ],
) )
agent_with_wmi = agent.extend(wmi_detail=get_wmi_data()) agent_with_wmi = agent.extend(wmi=get_wmi_data())

View File

@@ -1,83 +0,0 @@
from agents.models import Agent, AgentHistory
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.contrib.auth.models import AnonymousUser
from django.shortcuts import get_object_or_404
from tacticalrmm.constants import AGENT_DEFER, AgentHistoryType
from tacticalrmm.permissions import _has_perm_on_agent
class SendCMD(AsyncJsonWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
if isinstance(self.user, AnonymousUser):
await self.close()
await self.accept()
async def receive_json(self, payload, **kwargs):
auth = await self.has_perm(payload["agent_id"])
if not auth:
await self.send_json(
{"ret": "You do not have permission to perform this action."}
)
return
agent = await self.get_agent(payload["agent_id"])
timeout = int(payload["timeout"])
if payload["shell"] == "custom" and payload["custom_shell"]:
shell = payload["custom_shell"]
else:
shell = payload["shell"]
hist_pk = await self.get_history_id(agent, payload["cmd"])
data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": payload["cmd"],
"shell": shell,
},
"id": hist_pk,
}
ret = await agent.nats_cmd(data, timeout=timeout + 2)
await self.send_json({"ret": ret})
async def disconnect(self, _):
await self.close()
def _has_perm(self, perm: str) -> bool:
if self.user.is_superuser or (
self.user.role and getattr(self.user.role, "is_superuser")
):
return True
# make sure non-superusers with empty roles aren't permitted
elif not self.user.role:
return False
return self.user.role and getattr(self.user.role, perm)
@database_sync_to_async # type: ignore
def get_agent(self, agent_id: str) -> "Agent":
return get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id)
@database_sync_to_async # type: ignore
def get_history_id(self, agent: "Agent", cmd: str) -> int:
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.CMD_RUN,
command=cmd,
username=self.user.username[:50],
)
return hist.pk
@database_sync_to_async # type: ignore
def has_perm(self, agent_id: str) -> bool:
return self._has_perm("can_send_cmd") and _has_perm_on_agent(
self.user, agent_id
)

View File

@@ -5,8 +5,7 @@ from django.utils import timezone as djangotime
from packaging import version as pyver from packaging import version as pyver
from agents.models import Agent from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER from tacticalrmm.utils import AGENT_DEFER, reload_nats
from tacticalrmm.utils import reload_nats
class Command(BaseCommand): class Command(BaseCommand):

View File

@@ -3,9 +3,7 @@ import random
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from agents.models import Agent from agents.models import Agent
from core.tasks import cache_db_fields_task, handle_resolved_stuff
class Command(BaseCommand): class Command(BaseCommand):
@@ -24,10 +22,15 @@ class Command(BaseCommand):
rand = now - djangotime.timedelta(minutes=random.randint(10, 20)) rand = now - djangotime.timedelta(minutes=random.randint(10, 20))
random_dates.append(rand) random_dates.append(rand)
""" for _ in range(5):
rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10))
random_dates.append(rand)
for _ in range(5):
rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90))
random_dates.append(rand) """
agents = Agent.objects.only("last_seen") agents = Agent.objects.only("last_seen")
for agent in agents: for agent in agents:
agent.last_seen = random.choice(random_dates) agent.last_seen = random.choice(random_dates)
agent.save(update_fields=["last_seen"]) agent.save(update_fields=["last_seen"])
cache_db_fields_task()
handle_resolved_stuff()

View File

@@ -1,56 +1,32 @@
import datetime as dt
import json import json
import random import random
import string import string
import datetime as dt
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from django.conf import settings
from accounts.models import User from accounts.models import User
from agents.models import Agent, AgentHistory from agents.models import Agent, AgentHistory
from automation.models import Policy
from autotasks.models import AutomatedTask, TaskResult
from checks.models import Check, CheckHistory, CheckResult
from clients.models import Client, Site from clients.models import Client, Site
from logs.models import AuditLog, PendingAction
from scripts.models import Script
from software.models import InstalledSoftware from software.models import InstalledSoftware
from tacticalrmm.constants import ( from winupdate.models import WinUpdate, WinUpdatePolicy
AgentHistoryType, from checks.models import Check, CheckHistory
AgentMonType, from scripts.models import Script
AgentPlat, from autotasks.models import AutomatedTask
AlertSeverity, from automation.models import Policy
CheckStatus, from logs.models import PendingAction, AuditLog
CheckType,
EvtLogFailWhen,
EvtLogNames,
EvtLogTypes,
PAAction,
ScriptShell,
TaskSyncStatus,
TaskType,
)
from tacticalrmm.demo_data import ( from tacticalrmm.demo_data import (
check_network_loc_aware_ps1,
check_storage_pool_health_ps1,
clear_print_spool_bat,
disks, disks,
disks_linux_deb, temp_dir_stdout,
disks_linux_pi, spooler_stdout,
ping_fail_output, ping_fail_output,
ping_success_output, ping_success_output,
restart_nla_ps1,
show_temp_dir_py,
spooler_stdout,
temp_dir_stdout,
wmi_deb,
wmi_pi,
) )
from winupdate.models import WinUpdate, WinUpdatePolicy
AGENTS_TO_GENERATE = 20 AGENTS_TO_GENERATE = 250
SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json") WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json")
@@ -67,19 +43,18 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath(
class Command(BaseCommand): class Command(BaseCommand):
help = "populate database with fake agents" help = "populate database with fake agents"
def rand_string(self, length: int) -> str: def rand_string(self, length):
chars = string.ascii_letters chars = string.ascii_letters
return "".join(random.choice(chars) for _ in range(length)) return "".join(random.choice(chars) for _ in range(length))
def handle(self, *args, **kwargs) -> None: def handle(self, *args, **kwargs):
user = User.objects.first() user = User.objects.first()
if user:
user.totp_key = "ABSA234234" user.totp_key = "ABSA234234"
user.save(update_fields=["totp_key"]) user.save(update_fields=["totp_key"])
Agent.objects.all().delete()
Client.objects.all().delete() Client.objects.all().delete()
Agent.objects.all().delete()
Check.objects.all().delete() Check.objects.all().delete()
Script.objects.all().delete() Script.objects.all().delete()
AutomatedTask.objects.all().delete() AutomatedTask.objects.all().delete()
@@ -88,10 +63,7 @@ class Command(BaseCommand):
AuditLog.objects.all().delete() AuditLog.objects.all().delete()
PendingAction.objects.all().delete() PendingAction.objects.all().delete()
call_command("load_community_scripts") Script.load_community_scripts()
call_command("initial_db_setup")
call_command("load_chocos")
call_command("create_installer_user")
# policies # policies
check_policy = Policy() check_policy = Policy()
@@ -122,27 +94,27 @@ class Command(BaseCommand):
update_policy.email_if_fail = True update_policy.email_if_fail = True
update_policy.save() update_policy.save()
clients = ( clients = [
"Company 1",
"Company 2", "Company 2",
"Company 3", "Company 3",
"Company 1",
"Company 4", "Company 4",
"Company 5", "Company 5",
"Company 6", "Company 6",
) ]
sites1 = ("HQ1", "LA Office 1", "NY Office 1") sites1 = ["HQ1", "LA Office 1", "NY Office 1"]
sites2 = ("HQ2", "LA Office 2", "NY Office 2") sites2 = ["HQ2", "LA Office 2", "NY Office 2"]
sites3 = ("HQ3", "LA Office 3", "NY Office 3") sites3 = ["HQ3", "LA Office 3", "NY Office 3"]
sites4 = ("HQ4", "LA Office 4", "NY Office 4") sites4 = ["HQ4", "LA Office 4", "NY Office 4"]
sites5 = ("HQ5", "LA Office 5", "NY Office 5") sites5 = ["HQ5", "LA Office 5", "NY Office 5"]
sites6 = ("HQ6", "LA Office 6", "NY Office 6") sites6 = ["HQ6", "LA Office 6", "NY Office 6"]
client1 = Client(name=clients[0]) client1 = Client(name="Company 1")
client2 = Client(name=clients[1]) client2 = Client(name="Company 2")
client3 = Client(name=clients[2]) client3 = Client(name="Company 3")
client4 = Client(name=clients[3]) client4 = Client(name="Company 4")
client5 = Client(name=clients[4]) client5 = Client(name="Company 5")
client6 = Client(name=clients[5]) client6 = Client(name="Company 6")
client1.save() client1.save()
client2.save() client2.save()
@@ -169,7 +141,7 @@ class Command(BaseCommand):
for site in sites6: for site in sites6:
Site(client=client6, name=site).save() Site(client=client6, name=site).save()
hostnames = ( hostnames = [
"DC-1", "DC-1",
"DC-2", "DC-2",
"FSV-1", "FSV-1",
@@ -177,30 +149,27 @@ class Command(BaseCommand):
"WSUS", "WSUS",
"DESKTOP-12345", "DESKTOP-12345",
"LAPTOP-55443", "LAPTOP-55443",
) ]
descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop") descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"]
modes = AgentMonType.values modes = ["server", "workstation"]
op_systems_servers = ( op_systems_servers = [
"Microsoft Windows Server 2016 Standard, 64bit (build 14393)", "Microsoft Windows Server 2016 Standard, 64bit (build 14393)",
"Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)", "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)",
"Microsoft Windows Server 2019 Standard, 64bit (build 17763)", "Microsoft Windows Server 2019 Standard, 64bit (build 17763)",
) ]
op_systems_workstations = ( op_systems_workstations = [
"Microsoft Windows 8.1 Pro, 64bit (build 9600)", "Microsoft Windows 8.1 Pro, 64bit (build 9600)",
"Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)", "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)",
"Microsoft Windows 10 Pro, 64bit (build 18363)", "Microsoft Windows 10 Pro, 64bit (build 18363)",
) ]
linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64" public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45") total_rams = [4, 8, 16, 32, 64, 128]
used_rams = [10, 13, 60, 25, 76, 34, 56, 34, 39]
total_rams = (4, 8, 16, 32, 64, 128)
now = dt.datetime.now() now = dt.datetime.now()
django_now = djangotime.now()
boot_times = [] boot_times = []
@@ -212,7 +181,7 @@ class Command(BaseCommand):
rand_days = now - dt.timedelta(days=random.randint(2, 50)) rand_days = now - dt.timedelta(days=random.randint(2, 50))
boot_times.append(str(rand_days.timestamp())) boot_times.append(str(rand_days.timestamp()))
user_names = ("None", "Karen", "Steve", "jsmith", "jdoe") user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"]
with open(SVCS) as f: with open(SVCS) as f:
services = json.load(f) services = json.load(f)
@@ -227,7 +196,10 @@ class Command(BaseCommand):
with open(WMI_3) as f: with open(WMI_3) as f:
wmi3 = json.load(f) wmi3 = json.load(f)
wmi_details = [i for i in (wmi1, wmi2, wmi3)] wmi_details = []
wmi_details.append(wmi1)
wmi_details.append(wmi2)
wmi_details.append(wmi3)
# software # software
with open(SW_1) as f: with open(SW_1) as f:
@@ -236,7 +208,9 @@ class Command(BaseCommand):
with open(SW_2) as f: with open(SW_2) as f:
software2 = json.load(f) software2 = json.load(f)
softwares = [i for i in (software1, software2)] softwares = []
softwares.append(software1)
softwares.append(software2)
# windows updates # windows updates
with open(WIN_UPDATES) as f: with open(WIN_UPDATES) as f:
@@ -252,126 +226,111 @@ class Command(BaseCommand):
clear_spool.name = "Clear Print Spooler" clear_spool.name = "Clear Print Spooler"
clear_spool.description = "clears the print spooler. Fuck printers" clear_spool.description = "clears the print spooler. Fuck printers"
clear_spool.filename = "clear_print_spool.bat" clear_spool.filename = "clear_print_spool.bat"
clear_spool.shell = ScriptShell.CMD clear_spool.shell = "cmd"
clear_spool.script_body = clear_print_spool_bat
clear_spool.save() clear_spool.save()
check_net_aware = Script() check_net_aware = Script()
check_net_aware.name = "Check Network Location Awareness" check_net_aware.name = "Check Network Location Awareness"
check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails." check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails."
check_net_aware.filename = "check_network_loc_aware.ps1" check_net_aware.filename = "check_network_loc_aware.ps1"
check_net_aware.shell = ScriptShell.POWERSHELL check_net_aware.shell = "powershell"
check_net_aware.script_body = check_network_loc_aware_ps1
check_net_aware.save() check_net_aware.save()
check_pool_health = Script() check_pool_health = Script()
check_pool_health.name = "Check storage spool health" check_pool_health.name = "Check storage spool health"
check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy" check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy"
check_pool_health.filename = "check_storage_pool_health.ps1" check_pool_health.filename = "check_storage_pool_health.ps1"
check_pool_health.shell = ScriptShell.POWERSHELL check_pool_health.shell = "powershell"
check_pool_health.script_body = check_storage_pool_health_ps1
check_pool_health.save() check_pool_health.save()
restart_nla = Script() restart_nla = Script()
restart_nla.name = "Restart NLA Service" restart_nla.name = "Restart NLA Service"
restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails" restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails"
restart_nla.filename = "restart_nla.ps1" restart_nla.filename = "restart_nla.ps1"
restart_nla.shell = ScriptShell.POWERSHELL restart_nla.shell = "powershell"
restart_nla.script_body = restart_nla_ps1
restart_nla.save() restart_nla.save()
show_tmp_dir_script = Script() show_tmp_dir_script = Script()
show_tmp_dir_script.name = "Check temp dir" show_tmp_dir_script.name = "Check temp dir"
show_tmp_dir_script.description = "shows files in temp dir using python" show_tmp_dir_script.description = "shows files in temp dir using python"
show_tmp_dir_script.filename = "show_temp_dir.py" show_tmp_dir_script.filename = "show_temp_dir.py"
show_tmp_dir_script.shell = ScriptShell.PYTHON show_tmp_dir_script.shell = "python"
show_tmp_dir_script.script_body = show_temp_dir_py
show_tmp_dir_script.save() show_tmp_dir_script.save()
for count_agents in range(AGENTS_TO_GENERATE): for count_agents in range(AGENTS_TO_GENERATE):
client = random.choice(clients) client = random.choice(clients)
if client == clients[0]: if client == "Company 1":
site = random.choice(sites1) site = random.choice(sites1)
elif client == clients[1]: elif client == "Company 2":
site = random.choice(sites2) site = random.choice(sites2)
elif client == clients[2]: elif client == "Company 3":
site = random.choice(sites3) site = random.choice(sites3)
elif client == clients[3]: elif client == "Company 4":
site = random.choice(sites4) site = random.choice(sites4)
elif client == clients[4]: elif client == "Company 5":
site = random.choice(sites5) site = random.choice(sites5)
elif client == clients[5]: elif client == "Company 6":
site = random.choice(sites6) site = random.choice(sites6)
agent = Agent() agent = Agent()
plat_pick = random.randint(1, 15)
if plat_pick in (7, 11):
agent.plat = AgentPlat.LINUX
mode = AgentMonType.SERVER
# pi arm
if plat_pick == 7:
agent.goarch = "arm"
agent.wmi_detail = wmi_pi
agent.disks = disks_linux_pi
agent.operating_system = linux_pi_os
else:
agent.goarch = "amd64"
agent.wmi_detail = wmi_deb
agent.disks = disks_linux_deb
agent.operating_system = linux_deb_os
else:
agent.plat = AgentPlat.WINDOWS
agent.goarch = "amd64"
mode = random.choice(modes) mode = random.choice(modes)
agent.wmi_detail = random.choice(wmi_details) if mode == "server":
agent.services = services
agent.disks = random.choice(disks)
if mode == AgentMonType.SERVER:
agent.operating_system = random.choice(op_systems_servers) agent.operating_system = random.choice(op_systems_servers)
else: else:
agent.operating_system = random.choice(op_systems_workstations) agent.operating_system = random.choice(op_systems_workstations)
agent.hostname = random.choice(hostnames) agent.hostname = random.choice(hostnames)
agent.version = settings.LATEST_AGENT_VER agent.version = settings.LATEST_AGENT_VER
agent.salt_ver = "1.1.0"
agent.site = Site.objects.get(name=site) agent.site = Site.objects.get(name=site)
agent.agent_id = self.rand_string(40) agent.agent_id = self.rand_string(25)
agent.description = random.choice(descriptions) agent.description = random.choice(descriptions)
agent.monitoring_type = mode agent.monitoring_type = mode
agent.public_ip = random.choice(public_ips) agent.public_ip = random.choice(public_ips)
agent.last_seen = django_now agent.last_seen = djangotime.now()
agent.plat = "windows"
agent.plat_release = "windows-2019Server"
agent.total_ram = random.choice(total_rams) agent.total_ram = random.choice(total_rams)
agent.used_ram = random.choice(used_rams)
agent.boot_time = random.choice(boot_times) agent.boot_time = random.choice(boot_times)
agent.logged_in_username = random.choice(user_names) agent.logged_in_username = random.choice(user_names)
agent.antivirus = "windowsdefender"
agent.mesh_node_id = ( agent.mesh_node_id = (
"3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb" "3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb"
) )
agent.overdue_email_alert = random.choice([True, False]) agent.overdue_email_alert = random.choice([True, False])
agent.overdue_text_alert = random.choice([True, False]) agent.overdue_text_alert = random.choice([True, False])
agent.needs_reboot = random.choice([True, False]) agent.needs_reboot = random.choice([True, False])
agent.wmi_detail = random.choice(wmi_details)
agent.services = services
agent.disks = random.choice(disks)
agent.salt_id = "not-used"
agent.save() agent.save()
if agent.plat == AgentPlat.WINDOWS:
InstalledSoftware(agent=agent, software=random.choice(softwares)).save() InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
if mode == AgentMonType.WORKSTATION: if mode == "workstation":
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
else: else:
WinUpdatePolicy(agent=agent).save() WinUpdatePolicy(agent=agent).save()
if agent.plat == AgentPlat.WINDOWS:
# windows updates load # windows updates load
guids = [i for i in windows_updates.keys()] guids = []
for k in windows_updates.keys():
guids.append(k)
for i in guids: for i in guids:
WinUpdate( WinUpdate(
agent=agent, agent=agent,
guid=i, guid=i,
kb=windows_updates[i]["KBs"][0], kb=windows_updates[i]["KBs"][0],
mandatory=windows_updates[i]["Mandatory"],
title=windows_updates[i]["Title"], title=windows_updates[i]["Title"],
needs_reboot=windows_updates[i]["NeedsReboot"],
installed=windows_updates[i]["Installed"], installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"], downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"], description=windows_updates[i]["Description"],
@@ -381,7 +340,7 @@ class Command(BaseCommand):
# agent histories # agent histories
hist = AgentHistory() hist = AgentHistory()
hist.agent = agent hist.agent = agent
hist.type = AgentHistoryType.CMD_RUN hist.type = "cmd_run"
hist.command = "ping google.com" hist.command = "ping google.com"
hist.username = "demo" hist.username = "demo"
hist.results = ping_success_output hist.results = ping_success_output
@@ -389,7 +348,7 @@ class Command(BaseCommand):
hist1 = AgentHistory() hist1 = AgentHistory()
hist1.agent = agent hist1.agent = agent
hist1.type = AgentHistoryType.SCRIPT_RUN hist1.type = "script_run"
hist1.script = clear_spool hist1.script = clear_spool
hist1.script_results = { hist1.script_results = {
"id": 1, "id": 1,
@@ -400,11 +359,13 @@ class Command(BaseCommand):
} }
hist1.save() hist1.save()
if agent.plat == AgentPlat.WINDOWS:
# disk space check # disk space check
check1 = Check() check1 = Check()
check1.agent = agent check1.agent = agent
check1.check_type = CheckType.DISK_SPACE check1.check_type = "diskspace"
check1.status = "passing"
check1.last_run = djangotime.now()
check1.more_info = "Total: 498.7GB, Free: 287.4GB"
check1.warning_threshold = 25 check1.warning_threshold = 25
check1.error_threshold = 10 check1.error_threshold = 10
check1.disk = "C:" check1.disk = "C:"
@@ -412,56 +373,42 @@ class Command(BaseCommand):
check1.text_alert = random.choice([True, False]) check1.text_alert = random.choice([True, False])
check1.save() check1.save()
check_result1 = CheckResult()
check_result1.agent = agent
check_result1.assigned_check = check1
check_result1.status = CheckStatus.PASSING
check_result1.last_run = django_now
check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
check_result1.save()
for i in range(30): for i in range(30):
check1_history = CheckHistory() check1_history = CheckHistory()
check1_history.check_id = check1.pk check1_history.check_id = check1.id
check1_history.agent_id = agent.agent_id check1_history.x = djangotime.now() - djangotime.timedelta(
check1_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
check1_history.y = random.randint(13, 40) check1_history.y = random.randint(13, 40)
check1_history.save() check1_history.save()
# ping check # ping check
check2 = Check() check2 = Check()
check_result2 = CheckResult()
check2.agent = agent check2.agent = agent
check2.check_type = CheckType.PING check2.check_type = "ping"
check2.last_run = djangotime.now()
check2.email_alert = random.choice([True, False]) check2.email_alert = random.choice([True, False])
check2.text_alert = random.choice([True, False]) check2.text_alert = random.choice([True, False])
check_result2.agent = agent
check_result2.assigned_check = check2
check_result2.last_run = django_now
if site in sites5: if site in sites5:
check2.name = "Synology NAS" check2.name = "Synology NAS"
check2.alert_severity = AlertSeverity.ERROR check2.status = "failing"
check_result2.status = CheckStatus.FAILING
check2.ip = "172.17.14.26" check2.ip = "172.17.14.26"
check_result2.more_info = ping_fail_output check2.more_info = ping_fail_output
else: else:
check2.name = "Google" check2.name = "Google"
check_result2.status = CheckStatus.PASSING check2.status = "passing"
check2.ip = "8.8.8.8" check2.ip = "8.8.8.8"
check_result2.more_info = ping_success_output check2.more_info = ping_success_output
check2.save() check2.save()
check_result2.save()
for i in range(30): for i in range(30):
check2_history = CheckHistory() check2_history = CheckHistory()
check2_history.check_id = check2.pk check2_history.check_id = check2.id
check2_history.agent_id = agent.agent_id check2_history.x = djangotime.now() - djangotime.timedelta(
check2_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
if site in sites5: if site in sites5:
check2_history.y = 1 check2_history.y = 1
check2_history.results = ping_fail_output check2_history.results = ping_fail_output
@@ -473,97 +420,66 @@ class Command(BaseCommand):
# cpu load check # cpu load check
check3 = Check() check3 = Check()
check3.agent = agent check3.agent = agent
check3.check_type = CheckType.CPU_LOAD check3.check_type = "cpuload"
check3.status = "passing"
check3.last_run = djangotime.now()
check3.warning_threshold = 70 check3.warning_threshold = 70
check3.error_threshold = 90 check3.error_threshold = 90
check3.history = [15, 23, 16, 22, 22, 27, 15, 23, 23, 20, 10, 10, 13, 34]
check3.email_alert = random.choice([True, False]) check3.email_alert = random.choice([True, False])
check3.text_alert = random.choice([True, False]) check3.text_alert = random.choice([True, False])
check3.save() check3.save()
check_result3 = CheckResult()
check_result3.agent = agent
check_result3.assigned_check = check3
check_result3.status = CheckStatus.PASSING
check_result3.last_run = django_now
check_result3.history = [
15,
23,
16,
22,
22,
27,
15,
23,
23,
20,
10,
10,
13,
34,
]
check_result3.save()
for i in range(30): for i in range(30):
check3_history = CheckHistory() check3_history = CheckHistory()
check3_history.check_id = check3.pk check3_history.check_id = check3.id
check3_history.agent_id = agent.agent_id check3_history.x = djangotime.now() - djangotime.timedelta(
check3_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
check3_history.y = random.randint(2, 79) check3_history.y = random.randint(2, 79)
check3_history.save() check3_history.save()
# memory check # memory check
check4 = Check() check4 = Check()
check4.agent = agent check4.agent = agent
check4.check_type = CheckType.MEMORY check4.check_type = "memory"
check4.status = "passing"
check4.warning_threshold = 70 check4.warning_threshold = 70
check4.error_threshold = 85 check4.error_threshold = 85
check4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
check4.email_alert = random.choice([True, False]) check4.email_alert = random.choice([True, False])
check4.text_alert = random.choice([True, False]) check4.text_alert = random.choice([True, False])
check4.save() check4.save()
check_result4 = CheckResult()
check_result4.agent = agent
check_result4.assigned_check = check4
check_result4.status = CheckStatus.PASSING
check_result4.last_run = django_now
check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
check_result4.save()
for i in range(30): for i in range(30):
check4_history = CheckHistory() check4_history = CheckHistory()
check4_history.check_id = check4.pk check4_history.check_id = check4.id
check4_history.agent_id = agent.agent_id check4_history.x = djangotime.now() - djangotime.timedelta(
check4_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
check4_history.y = random.randint(2, 79) check4_history.y = random.randint(2, 79)
check4_history.save() check4_history.save()
# script check storage pool # script check storage pool
check5 = Check() check5 = Check()
check5.agent = agent check5.agent = agent
check5.check_type = CheckType.SCRIPT check5.check_type = "script"
check5.status = "passing"
check5.last_run = djangotime.now()
check5.email_alert = random.choice([True, False]) check5.email_alert = random.choice([True, False])
check5.text_alert = random.choice([True, False]) check5.text_alert = random.choice([True, False])
check5.timeout = 120 check5.timeout = 120
check5.retcode = 0
check5.execution_time = "4.0000"
check5.script = check_pool_health check5.script = check_pool_health
check5.save() check5.save()
check_result5 = CheckResult()
check_result5.agent = agent
check_result5.assigned_check = check5
check_result5.status = CheckStatus.PASSING
check_result5.last_run = django_now
check_result5.retcode = 0
check_result5.execution_time = "4.0000"
check_result5.save()
for i in range(30): for i in range(30):
check5_history = CheckHistory() check5_history = CheckHistory()
check5_history.check_id = check5.pk check5_history.check_id = check5.id
check5_history.agent_id = agent.agent_id check5_history.x = djangotime.now() - djangotime.timedelta(
check5_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
if i == 10 or i == 18: if i == 10 or i == 18:
check5_history.y = 1 check5_history.y = 1
else: else:
@@ -571,155 +487,98 @@ class Command(BaseCommand):
check5_history.save() check5_history.save()
check6 = Check() check6 = Check()
check6.agent = agent check6.agent = agent
check6.check_type = CheckType.SCRIPT check6.check_type = "script"
check6.status = "passing"
check6.last_run = djangotime.now()
check6.email_alert = random.choice([True, False]) check6.email_alert = random.choice([True, False])
check6.text_alert = random.choice([True, False]) check6.text_alert = random.choice([True, False])
check6.timeout = 120 check6.timeout = 120
check6.retcode = 0
check6.execution_time = "4.0000"
check6.script = check_net_aware check6.script = check_net_aware
check6.save() check6.save()
check_result6 = CheckResult()
check_result6.agent = agent
check_result6.assigned_check = check6
check_result6.status = CheckStatus.PASSING
check_result6.last_run = django_now
check_result6.retcode = 0
check_result6.execution_time = "4.0000"
check_result6.save()
for i in range(30): for i in range(30):
check6_history = CheckHistory() check6_history = CheckHistory()
check6_history.check_id = check6.pk check6_history.check_id = check6.id
check6_history.agent_id = agent.agent_id check6_history.x = djangotime.now() - djangotime.timedelta(
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
check6_history.y = 0 check6_history.y = 0
check6_history.save() check6_history.save()
nla_task = AutomatedTask() nla_task = AutomatedTask()
nla_task.agent = agent nla_task.agent = agent
actions = [ nla_task.script = restart_nla
{
"name": restart_nla.name,
"type": "script",
"script": restart_nla.pk,
"timeout": 90,
"script_args": [],
}
]
nla_task.actions = actions
nla_task.assigned_check = check6 nla_task.assigned_check = check6
nla_task.name = "Restart NLA" nla_task.name = "Restart NLA"
nla_task.task_type = TaskType.CHECK_FAILURE nla_task.task_type = "checkfailure"
nla_task.win_task_name = "demotask123"
nla_task.execution_time = "1.8443"
nla_task.last_run = djangotime.now()
nla_task.stdout = "no stdout"
nla_task.retcode = 0
nla_task.sync_status = "synced"
nla_task.save() nla_task.save()
nla_task_result = TaskResult()
nla_task_result.task = nla_task
nla_task_result.agent = agent
nla_task_result.execution_time = "1.8443"
nla_task_result.last_run = django_now
nla_task_result.stdout = "no stdout"
nla_task_result.retcode = 0
nla_task_result.sync_status = TaskSyncStatus.SYNCED
nla_task_result.save()
spool_task = AutomatedTask() spool_task = AutomatedTask()
spool_task.agent = agent spool_task.agent = agent
actions = [ spool_task.script = clear_spool
{
"name": clear_spool.name,
"type": "script",
"script": clear_spool.pk,
"timeout": 90,
"script_args": [],
}
]
spool_task.actions = actions
spool_task.name = "Clear the print spooler" spool_task.name = "Clear the print spooler"
spool_task.task_type = TaskType.DAILY spool_task.task_type = "scheduled"
spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10) spool_task.run_time_bit_weekdays = 127
spool_task.expire_date = django_now + djangotime.timedelta(days=753) spool_task.run_time_minute = "04:45"
spool_task.daily_interval = 1 spool_task.win_task_name = "demospool123"
spool_task.weekly_interval = 1 spool_task.last_run = djangotime.now()
spool_task.task_repetition_duration = "2h" spool_task.retcode = 0
spool_task.task_repetition_interval = "25m" spool_task.stdout = spooler_stdout
spool_task.random_task_delay = "3m" spool_task.sync_status = "synced"
spool_task.save() spool_task.save()
spool_task_result = TaskResult()
spool_task_result.task = spool_task
spool_task_result.agent = agent
spool_task_result.last_run = django_now
spool_task_result.retcode = 0
spool_task_result.stdout = spooler_stdout
spool_task_result.sync_status = TaskSyncStatus.SYNCED
spool_task_result.save()
tmp_dir_task = AutomatedTask() tmp_dir_task = AutomatedTask()
tmp_dir_task.agent = agent tmp_dir_task.agent = agent
tmp_dir_task.name = "show temp dir files" tmp_dir_task.name = "show temp dir files"
actions = [ tmp_dir_task.script = show_tmp_dir_script
{ tmp_dir_task.task_type = "manual"
"name": show_tmp_dir_script.name, tmp_dir_task.win_task_name = "demotemp"
"type": "script", tmp_dir_task.last_run = djangotime.now()
"script": show_tmp_dir_script.pk, tmp_dir_task.stdout = temp_dir_stdout
"timeout": 90, tmp_dir_task.retcode = 0
"script_args": [], tmp_dir_task.sync_status = "synced"
}
]
tmp_dir_task.actions = actions
tmp_dir_task.task_type = TaskType.MANUAL
tmp_dir_task.save() tmp_dir_task.save()
tmp_dir_task_result = TaskResult()
tmp_dir_task_result.task = tmp_dir_task
tmp_dir_task_result.agent = agent
tmp_dir_task_result.last_run = django_now
tmp_dir_task_result.stdout = temp_dir_stdout
tmp_dir_task_result.retcode = 0
tmp_dir_task_result.sync_status = TaskSyncStatus.SYNCED
tmp_dir_task_result.save()
check7 = Check() check7 = Check()
check7.agent = agent check7.agent = agent
check7.check_type = CheckType.SCRIPT check7.check_type = "script"
check7.status = "passing"
check7.last_run = djangotime.now()
check7.email_alert = random.choice([True, False]) check7.email_alert = random.choice([True, False])
check7.text_alert = random.choice([True, False]) check7.text_alert = random.choice([True, False])
check7.timeout = 120 check7.timeout = 120
check7.retcode = 0
check7.execution_time = "3.1337"
check7.script = clear_spool check7.script = clear_spool
check7.stdout = spooler_stdout
check7.save() check7.save()
check_result7 = CheckResult()
check_result7.assigned_check = check7
check_result7.agent = agent
check_result7.status = CheckStatus.PASSING
check_result7.last_run = django_now
check_result7.retcode = 0
check_result7.execution_time = "3.1337"
check_result7.stdout = spooler_stdout
check_result7.save()
for i in range(30): for i in range(30):
check7_history = CheckHistory() check7_history = CheckHistory()
check7_history.check_id = check7.pk check7_history.check_id = check7.id
check7_history.agent_id = agent.agent_id check7_history.x = djangotime.now() - djangotime.timedelta(
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
check7_history.y = 0 check7_history.y = 0
check7_history.save() check7_history.save()
if agent.plat == AgentPlat.WINDOWS:
check8 = Check() check8 = Check()
check8.agent = agent check8.agent = agent
check8.check_type = CheckType.WINSVC check8.check_type = "winsvc"
check8.status = "passing"
check8.last_run = djangotime.now()
check8.email_alert = random.choice([True, False]) check8.email_alert = random.choice([True, False])
check8.text_alert = random.choice([True, False]) check8.text_alert = random.choice([True, False])
check8.more_info = "Status RUNNING"
check8.fails_b4_alert = 4 check8.fails_b4_alert = 4
check8.svc_name = "Spooler" check8.svc_name = "Spooler"
check8.svc_display_name = "Print Spooler" check8.svc_display_name = "Print Spooler"
@@ -727,19 +586,12 @@ class Command(BaseCommand):
check8.restart_if_stopped = True check8.restart_if_stopped = True
check8.save() check8.save()
check_result8 = CheckResult()
check_result8.assigned_check = check8
check_result8.agent = agent
check_result8.status = CheckStatus.PASSING
check_result8.last_run = django_now
check_result8.more_info = "Status RUNNING"
check_result8.save()
for i in range(30): for i in range(30):
check8_history = CheckHistory() check8_history = CheckHistory()
check8_history.check_id = check8.pk check8_history.check_id = check8.id
check8_history.agent_id = agent.agent_id check8_history.x = djangotime.now() - djangotime.timedelta(
check8_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
if i == 10 or i == 18: if i == 10 or i == 18:
check8_history.y = 1 check8_history.y = 1
check8_history.results = "Status STOPPED" check8_history.results = "Status STOPPED"
@@ -750,37 +602,35 @@ class Command(BaseCommand):
check9 = Check() check9 = Check()
check9.agent = agent check9.agent = agent
check9.check_type = CheckType.EVENT_LOG check9.check_type = "eventlog"
check9.name = "unexpected shutdown" check9.name = "unexpected shutdown"
check9.last_run = djangotime.now()
check9.email_alert = random.choice([True, False]) check9.email_alert = random.choice([True, False])
check9.text_alert = random.choice([True, False]) check9.text_alert = random.choice([True, False])
check9.fails_b4_alert = 2 check9.fails_b4_alert = 2
check9.log_name = EvtLogNames.APPLICATION
if site in sites5:
check9.extra_details = eventlog_check_fail_data
check9.status = "failing"
else:
check9.extra_details = {"log": []}
check9.status = "passing"
check9.log_name = "Application"
check9.event_id = 1001 check9.event_id = 1001
check9.event_type = EvtLogTypes.INFO check9.event_type = "INFO"
check9.fail_when = EvtLogFailWhen.CONTAINS check9.fail_when = "contains"
check9.search_last_days = 30 check9.search_last_days = 30
check_result9 = CheckResult()
check_result9.agent = agent
check_result9.assigned_check = check9
check_result9.last_run = django_now
if site in sites5:
check_result9.extra_details = eventlog_check_fail_data
check_result9.status = CheckStatus.FAILING
else:
check_result9.extra_details = {"log": []}
check_result9.status = CheckStatus.PASSING
check9.save() check9.save()
check_result9.save()
for i in range(30): for i in range(30):
check9_history = CheckHistory() check9_history = CheckHistory()
check9_history.check_id = check9.pk check9_history.check_id = check9.id
check9_history.agent_id = agent.agent_id check9_history.x = djangotime.now() - djangotime.timedelta(
check9_history.x = django_now - djangotime.timedelta(minutes=i * 2) minutes=i * 2
)
if i == 10 or i == 18: if i == 10 or i == 18:
check9_history.y = 1 check9_history.y = 1
check9_history.results = "Events Found: 16" check9_history.results = "Events Found: 16"
@@ -793,7 +643,7 @@ class Command(BaseCommand):
if pick == 5 or pick == 3: if pick == 5 or pick == 3:
reboot_time = django_now + djangotime.timedelta( reboot_time = djangotime.now() + djangotime.timedelta(
minutes=random.randint(1000, 500000) minutes=random.randint(1000, 500000)
) )
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M") date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
@@ -806,7 +656,7 @@ class Command(BaseCommand):
sched_reboot = PendingAction() sched_reboot = PendingAction()
sched_reboot.agent = agent sched_reboot.agent = agent
sched_reboot.action_type = PAAction.SCHED_REBOOT sched_reboot.action_type = "schedreboot"
sched_reboot.details = { sched_reboot.details = {
"time": str(obj), "time": str(obj),
"taskname": task_name, "taskname": task_name,

View File

@@ -1,30 +0,0 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):
help = "Find all agents that have a certain service installed"
def add_arguments(self, parser):
parser.add_argument("name", type=str)
def handle(self, *args, **kwargs):
search = kwargs["name"].lower()
agents = Agent.objects.defer(*AGENT_DEFER)
for agent in agents:
try:
for svc in agent.services:
if (
search in svc["name"].lower()
or search in svc["display_name"].lower()
):
self.stdout.write(
self.style.SUCCESS(
f"{agent.hostname} - {svc['name']} ({svc['display_name']}) - {svc['status']}"
)
)
except:
continue

View File

@@ -0,0 +1,16 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
class Command(BaseCommand):
help = "Changes existing agents salt_id from a property to a model field"
def handle(self, *args, **kwargs):
agents = Agent.objects.filter(salt_id=None)
for agent in agents:
self.stdout.write(
self.style.SUCCESS(f"Setting salt_id on {agent.hostname}")
)
agent.salt_id = f"{agent.hostname}-{agent.pk}"
agent.save(update_fields=["salt_id"])

View File

@@ -2,16 +2,16 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from agents.models import Agent from agents.models import Agent
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ONLINE_AGENTS
class Command(BaseCommand): class Command(BaseCommand):
help = "Shows online agents that are not on the latest version" help = "Shows online agents that are not on the latest version"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
only = ONLINE_AGENTS + ("hostname",) q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(*only) "pk", "version", "last_seen", "overdue_time", "offline_time"
agents = [i for i in q if i.status == AGENT_STATUS_ONLINE] )
agents = [i for i in q if i.status == "online"]
for agent in agents: for agent in agents:
self.stdout.write( self.stdout.write(
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}") self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")

View File

@@ -3,17 +3,17 @@ from django.core.management.base import BaseCommand
from packaging import version as pyver from packaging import version as pyver
from agents.models import Agent from agents.models import Agent
from core.models import CoreSettings
from agents.tasks import send_agent_update_task from agents.tasks import send_agent_update_task
from core.utils import get_core_settings, token_is_valid from tacticalrmm.utils import AGENT_DEFER
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand): class Command(BaseCommand):
help = "Triggers an agent update task to run" help = "Triggers an agent update task to run"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
core = get_core_settings() core = CoreSettings.objects.first()
if not core.agent_auto_update: if not core.agent_auto_update: # type: ignore
return return
q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER) q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER)
@@ -22,5 +22,4 @@ class Command(BaseCommand):
for i in q for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
] ]
token, _ = token_is_valid() send_agent_update_task.delay(agent_ids=agent_ids)
send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False)

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.1 on 2021-07-06 02:01 # Generated by Django 3.2.1 on 2021-07-06 02:01
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.2.5 on 2021-07-14 07:38 # Generated by Django 3.2.5 on 2021-07-14 07:38
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,25 +0,0 @@
# Generated by Django 3.2.12 on 2022-02-27 05:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0042_alter_agent_time_zone'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='antivirus',
),
migrations.RemoveField(
model_name='agent',
name='local_ip',
),
migrations.RemoveField(
model_name='agent',
name='used_ram',
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 3.2.12 on 2022-02-27 07:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0043_auto_20220227_0554'),
]
operations = [
migrations.RenameField(
model_name='agent',
old_name='salt_id',
new_name='goarch',
),
migrations.RemoveField(
model_name='agent',
name='salt_ver',
),
]

View File

@@ -1,16 +0,0 @@
# Generated by Django 3.2.12 on 2022-03-12 02:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0044_auto_20220227_0717'),
]
operations = [
migrations.DeleteModel(
name='RecoveryAction',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2022-03-17 17:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0045_delete_recoveryaction'),
]
operations = [
migrations.AlterField(
model_name='agenthistory',
name='command',
field=models.TextField(blank=True, default='', null=True),
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-07 17:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0020_auto_20211226_0547'),
('agents', '0046_alter_agenthistory_command'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='plat',
field=models.CharField(default='windows', max_length=255),
),
migrations.AlterField(
model_name='agent',
name='site',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.RESTRICT, related_name='agents', to='clients.site'),
preserve_default=False,
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-16 17:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0047_alter_agent_plat_alter_agent_site'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='has_patches_pending',
),
migrations.RemoveField(
model_name='agent',
name='pending_actions_count',
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-18 14:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0048_remove_agent_has_patches_pending_and_more'),
]
operations = [
migrations.AddIndex(
model_name='agent',
index=models.Index(fields=['monitoring_type'], name='agents_agen_monitor_df8816_idx'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 4.0.4 on 2022-04-25 06:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0049_agent_agents_agen_monitor_df8816_idx'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='plat_release',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-18 03:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0050_remove_agent_plat_release'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='plat',
field=models.CharField(choices=[('windows', 'Windows'), ('linux', 'Linux'), ('darwin', 'macOS')], default='windows', max_length=255),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-18 05:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0051_alter_agent_plat'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='monitoring_type',
field=models.CharField(choices=[('server', 'Server'), ('workstation', 'Workstation')], default='server', max_length=30),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-18 06:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0052_alter_agent_monitoring_type'),
]
operations = [
migrations.RemoveField(
model_name='agenthistory',
name='status',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-06-06 04:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0053_remove_agenthistory_status'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='goarch',
field=models.CharField(blank=True, choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], max_length=255, null=True),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class AgentPerms(permissions.BasePermission): class AgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
if "agent_id" in view.kwargs.keys(): if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_agents") and _has_perm_on_agent( return _has_perm(r, "can_list_agents") and _has_perm_on_agent(
@@ -26,76 +26,73 @@ class AgentPerms(permissions.BasePermission):
class RecoverAgentPerms(permissions.BasePermission): class RecoverAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if "agent_id" not in view.kwargs.keys():
return _has_perm(r, "can_recover_agents")
return _has_perm(r, "can_recover_agents") and _has_perm_on_agent( return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class MeshPerms(permissions.BasePermission): class MeshPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_use_mesh") and _has_perm_on_agent( return _has_perm(r, "can_use_mesh") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class UpdateAgentPerms(permissions.BasePermission): class UpdateAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_update_agents") return _has_perm(r, "can_update_agents")
class PingAgentPerms(permissions.BasePermission): class PingAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_ping_agents") and _has_perm_on_agent( return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class ManageProcPerms(permissions.BasePermission): class ManageProcPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_manage_procs") and _has_perm_on_agent( return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class EvtLogPerms(permissions.BasePermission): class EvtLogPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent( return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class SendCMDPerms(permissions.BasePermission): class SendCMDPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_send_cmd") and _has_perm_on_agent( return _has_perm(r, "can_send_cmd") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class RebootAgentPerms(permissions.BasePermission): class RebootAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent( return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class InstallAgentPerms(permissions.BasePermission): class InstallAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_install_agents") return _has_perm(r, "can_install_agents")
class RunScriptPerms(permissions.BasePermission): class RunScriptPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_run_scripts") and _has_perm_on_agent( return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
class AgentNotesPerms(permissions.BasePermission): class AgentNotesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
# permissions for GET /agents/notes/ endpoint # permissions for GET /agents/notes/ endpoint
if r.method == "GET": if r.method == "GET":
@@ -112,12 +109,12 @@ class AgentNotesPerms(permissions.BasePermission):
class RunBulkPerms(permissions.BasePermission): class RunBulkPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_run_bulk") return _has_perm(r, "can_run_bulk")
class AgentHistoryPerms(permissions.BasePermission): class AgentHistoryPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if "agent_id" in view.kwargs.keys(): if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent( return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]

View File

@@ -1,10 +1,8 @@
import pytz import pytz
from rest_framework import serializers from rest_framework import serializers
from tacticalrmm.constants import AGENT_STATUS_ONLINE
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, AgentCustomField, AgentHistory, Note from .models import Agent, AgentCustomField, Note, AgentHistory
class AgentCustomFieldSerializer(serializers.ModelSerializer): class AgentCustomFieldSerializer(serializers.ModelSerializer):
@@ -42,33 +40,6 @@ class AgentSerializer(serializers.ModelSerializer):
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
patches_last_installed = serializers.ReadOnlyField() patches_last_installed = serializers.ReadOnlyField()
last_seen = serializers.ReadOnlyField() last_seen = serializers.ReadOnlyField()
applied_policies = serializers.SerializerMethodField()
effective_patch_policy = serializers.SerializerMethodField()
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
from alerts.serializers import AlertTemplateSerializer
return (
AlertTemplateSerializer(obj.alert_template).data
if obj.alert_template
else None
)
def get_effective_patch_policy(self, obj):
return WinUpdatePolicySerializer(obj.get_patch_policy()).data
def get_applied_policies(self, obj):
from automation.serializers import PolicySerializer
policies = obj.get_agent_policies()
# need to serialize model objects manually
for key, policy in policies.items():
if policy:
policies[key] = PolicySerializer(policy).data
return policies
def get_all_timezones(self, obj): def get_all_timezones(self, obj):
return pytz.all_timezones return pytz.all_timezones
@@ -81,15 +52,13 @@ class AgentSerializer(serializers.ModelSerializer):
class AgentTableSerializer(serializers.ModelSerializer): class AgentTableSerializer(serializers.ModelSerializer):
status = serializers.ReadOnlyField() status = serializers.ReadOnlyField()
checks = serializers.ReadOnlyField() checks = serializers.ReadOnlyField()
last_seen = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name") client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name") site_name = serializers.ReadOnlyField(source="site.name")
logged_username = serializers.SerializerMethodField() logged_username = serializers.SerializerMethodField()
italic = serializers.SerializerMethodField() italic = serializers.SerializerMethodField()
policy = serializers.ReadOnlyField(source="policy.id") policy = serializers.ReadOnlyField(source="policy.id")
alert_template = serializers.SerializerMethodField() alert_template = serializers.SerializerMethodField()
last_seen = serializers.ReadOnlyField()
pending_actions_count = serializers.ReadOnlyField()
has_patches_pending = serializers.ReadOnlyField()
def get_alert_template(self, obj): def get_alert_template(self, obj):
@@ -103,8 +72,16 @@ class AgentTableSerializer(serializers.ModelSerializer):
"always_alert": obj.alert_template.agent_always_alert, "always_alert": obj.alert_template.agent_always_alert,
} }
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")
def get_logged_username(self, obj) -> str: def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE: if obj.logged_in_username == "None" and obj.status == "online":
return obj.last_logged_in_user return obj.last_logged_in_user
elif obj.logged_in_username != "None": elif obj.logged_in_username != "None":
return obj.logged_in_username return obj.logged_in_username
@@ -112,7 +89,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
return "-" return "-"
def get_italic(self, obj) -> bool: def get_italic(self, obj) -> bool:
return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE return obj.logged_in_username == "None" and obj.status == "online"
class Meta: class Meta:
model = Agent model = Agent
@@ -125,6 +102,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
"monitoring_type", "monitoring_type",
"description", "description",
"needs_reboot", "needs_reboot",
"has_patches_pending",
"pending_actions_count", "pending_actions_count",
"status", "status",
"overdue_text_alert", "overdue_text_alert",
@@ -138,9 +116,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
"italic", "italic",
"policy", "policy",
"block_policy_inheritance", "block_policy_inheritance",
"plat",
"goarch",
"has_patches_pending",
] ]
depth = 2 depth = 2
@@ -177,12 +152,17 @@ class AgentNoteSerializer(serializers.ModelSerializer):
class AgentHistorySerializer(serializers.ModelSerializer): class AgentHistorySerializer(serializers.ModelSerializer):
time = serializers.SerializerMethodField(read_only=True)
script_name = serializers.ReadOnlyField(source="script.name") script_name = serializers.ReadOnlyField(source="script.name")
class Meta: class Meta:
model = AgentHistory model = AgentHistory
fields = "__all__" fields = "__all__"
def get_time(self, history):
tz = self.context["default_tz"]
return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
class AgentAuditSerializer(serializers.ModelSerializer): class AgentAuditSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -1,52 +1,120 @@
import asyncio
import datetime as dt import datetime as dt
import random import random
from time import sleep from time import sleep
from typing import TYPE_CHECKING, Optional from typing import Union
from django.core.management import call_command from core.models import CoreSettings
from django.conf import settings
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from logs.models import DebugLog, PendingAction
from agents.models import Agent from packaging import version as pyver
from core.utils import get_core_settings
from logs.models import DebugLog
from scripts.models import Script from scripts.models import Script
from tacticalrmm.celery import app from tacticalrmm.celery import app
from tacticalrmm.constants import (
AGENT_DEFER, from agents.models import Agent
AGENT_STATUS_OVERDUE, from agents.utils import get_winagent_url
CheckStatus,
DebugLogType,
def agent_update(agent_id: str, force: bool = False) -> str:
agent = Agent.objects.get(agent_id=agent_id)
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
return "not supported"
# skip if we can't determine the arch
if agent.arch is None:
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
)
return "noarch"
version = settings.LATEST_AGENT_VER
inno = agent.win_inno_exe
url = get_winagent_url(agent.arch)
if not force:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
) )
if TYPE_CHECKING: nats_data = {
from django.db.models.query import QuerySet "func": "agentupdate",
"payload": {
"url": url,
"version": version,
"inno": inno,
},
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created"
@app.task @app.task
def send_agent_update_task(*, agent_ids: list[str], token: str, force: bool) -> None: def force_code_sign(agent_ids: list[str]) -> None:
agents: "QuerySet[Agent]" = Agent.objects.defer(*AGENT_DEFER).filter( chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
agent_id__in=agent_ids for chunk in chunks:
) for agent_id in chunk:
for agent in agents: agent_update(agent_id=agent_id, force=True)
agent.do_update(token=token, force=force) sleep(0.05)
sleep(4)
@app.task
def send_agent_update_task(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
sleep(0.05)
sleep(4)
@app.task @app.task
def auto_self_agent_update_task() -> None: def auto_self_agent_update_task() -> None:
call_command("update_agents") core = CoreSettings.objects.first()
if not core.agent_auto_update: # type:ignore
return
q = Agent.objects.only("agent_id", "version")
agent_ids: list[str] = [
i.agent_id
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
sleep(0.05)
sleep(4)
@app.task @app.task
def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) -> str: def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert from alerts.models import Alert
try:
alert = Alert.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
except Alert.DoesNotExist:
return "alert not found"
if not alert.email_sent: if not alert.email_sent:
sleep(random.randint(1, 5)) sleep(random.randint(1, 15))
alert.agent.send_outage_email() alert.agent.send_outage_email()
alert.email_sent = djangotime.now() alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"]) alert.save(update_fields=["email_sent"])
@@ -55,7 +123,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
# send an email only if the last email sent is older than alert interval # send an email only if the last email sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval) delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta: if alert.email_sent < delta:
sleep(random.randint(1, 5)) sleep(random.randint(1, 10))
alert.agent.send_outage_email() alert.agent.send_outage_email()
alert.email_sent = djangotime.now() alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"]) alert.save(update_fields=["email_sent"])
@@ -67,13 +135,8 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
def agent_recovery_email_task(pk: int) -> str: def agent_recovery_email_task(pk: int) -> str:
from alerts.models import Alert from alerts.models import Alert
sleep(random.randint(1, 5)) sleep(random.randint(1, 15))
try:
alert = Alert.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
except Alert.DoesNotExist:
return "alert not found"
alert.agent.send_recovery_email() alert.agent.send_recovery_email()
alert.resolved_email_sent = djangotime.now() alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"]) alert.save(update_fields=["resolved_email_sent"])
@@ -82,16 +145,13 @@ def agent_recovery_email_task(pk: int) -> str:
@app.task @app.task
def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> str: def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert from alerts.models import Alert
try:
alert = Alert.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
except Alert.DoesNotExist:
return "alert not found"
if not alert.sms_sent: if not alert.sms_sent:
sleep(random.randint(1, 3)) sleep(random.randint(1, 15))
alert.agent.send_outage_sms() alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now() alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"]) alert.save(update_fields=["sms_sent"])
@@ -100,7 +160,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
# send an sms only if the last sms sent is older than alert interval # send an sms only if the last sms sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval) delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta: if alert.sms_sent < delta:
sleep(random.randint(1, 3)) sleep(random.randint(1, 10))
alert.agent.send_outage_sms() alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now() alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"]) alert.save(update_fields=["sms_sent"])
@@ -113,11 +173,7 @@ def agent_recovery_sms_task(pk: int) -> str:
from alerts.models import Alert from alerts.models import Alert
sleep(random.randint(1, 3)) sleep(random.randint(1, 3))
try:
alert = Alert.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
except Alert.DoesNotExist:
return "alert not found"
alert.agent.send_recovery_sms() alert.agent.send_recovery_sms()
alert.resolved_sms_sent = djangotime.now() alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"]) alert.save(update_fields=["resolved_sms_sent"])
@@ -141,7 +197,7 @@ def agent_outages_task() -> None:
) )
for agent in agents: for agent in agents:
if agent.status == AGENT_STATUS_OVERDUE: if agent.status == "overdue":
Alert.handle_alert_failure(agent) Alert.handle_alert_failure(agent)
@@ -167,12 +223,12 @@ def run_script_email_results_task(
if r == "timeout": if r == "timeout":
DebugLog.error( DebugLog.error(
agent=agent, agent=agent,
log_type=DebugLogType.SCRIPTING, log_type="scripting",
message=f"{agent.hostname}({agent.pk}) timed out running script.", message=f"{agent.hostname}({agent.pk}) timed out running script.",
) )
return return
CORE = get_core_settings() CORE = CoreSettings.objects.first()
subject = f"{agent.hostname} {script.name} Results" subject = f"{agent.hostname} {script.name} Results"
exec_time = "{:.4f}".format(r["execution_time"]) exec_time = "{:.4f}".format(r["execution_time"])
body = ( body = (
@@ -185,21 +241,25 @@ def run_script_email_results_task(
msg = EmailMessage() msg = EmailMessage()
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = CORE.smtp_from_email msg["From"] = CORE.smtp_from_email # type:ignore
if emails: if emails:
msg["To"] = ", ".join(emails) msg["To"] = ", ".join(emails)
else: else:
msg["To"] = ", ".join(CORE.email_alert_recipients) msg["To"] = ", ".join(CORE.email_alert_recipients) # type:ignore
msg.set_content(body) msg.set_content(body)
try: try:
with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server: with smtplib.SMTP(
if CORE.smtp_requires_auth: CORE.smtp_host, CORE.smtp_port, timeout=20 # type:ignore
) as server: # type:ignore
if CORE.smtp_requires_auth: # type:ignore
server.ehlo() server.ehlo()
server.starttls() server.starttls()
server.login(CORE.smtp_host_user, CORE.smtp_host_password) server.login(
CORE.smtp_host_user, CORE.smtp_host_password # type:ignore
) # type:ignore
server.send_message(msg) server.send_message(msg)
server.quit() server.quit()
else: else:
@@ -211,22 +271,18 @@ def run_script_email_results_task(
@app.task @app.task
def clear_faults_task(older_than_days: int) -> None: def clear_faults_task(older_than_days: int) -> None:
from alerts.models import Alert # https://github.com/wh1te909/tacticalrmm/issues/484
# https://github.com/amidaware/tacticalrmm/issues/484
agents = Agent.objects.exclude(last_seen__isnull=True).filter( agents = Agent.objects.exclude(last_seen__isnull=True).filter(
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
) )
for agent in agents: for agent in agents:
for check in agent.get_checks_with_policies(): if agent.agentchecks.exists():
for check in agent.agentchecks.all():
# reset check status # reset check status
if check.check_result: check.status = "passing"
check.check_result.status = CheckStatus.PASSING check.save(update_fields=["status"])
check.check_result.save(update_fields=["status"]) if check.alert.filter(resolved=False).exists():
if check.alert.filter(agent=agent, resolved=False).exists(): check.alert.get(resolved=False).resolve()
alert = Alert.create_or_return_check_alert(check, agent=agent)
if alert:
alert.resolve()
# reset overdue alerts # reset overdue alerts
agent.overdue_email_alert = False agent.overdue_email_alert = False
@@ -250,8 +306,3 @@ def prune_agent_history(older_than_days: int) -> str:
).delete() ).delete()
return "ok" return "ok"
@app.task
def bulk_recover_agents_task() -> None:
call_command("bulk_restart_agents")

View File

@@ -1,106 +0,0 @@
from unittest.mock import patch
from rest_framework.response import Response
from tacticalrmm.test import TacticalTestCase
class TestAgentInstalls(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("agents.utils.generate_linux_install")
@patch("knox.models.AuthToken.objects.create")
@patch("tacticalrmm.utils.generate_winagent_exe")
@patch("core.utils.token_is_valid")
@patch("agents.utils.get_agent_url")
def test_install_agent(
self,
mock_agent_url,
mock_token_valid,
mock_gen_win_exe,
mock_auth,
mock_linux_install,
):
mock_agent_url.return_value = "https://example.com"
mock_token_valid.return_value = "", False
mock_gen_win_exe.return_value = Response("ok")
mock_auth.return_value = "", "token"
mock_linux_install.return_value = Response("ok")
url = "/agents/installer/"
# test windows dynamic exe
data = {
"installMethod": "exe",
"client": self.site2.client.pk,
"site": self.site2.pk,
"expires": 24,
"agenttype": "server",
"power": 0,
"rdp": 1,
"ping": 0,
"goarch": "amd64",
"api": "https://api.example.com",
"fileName": "rmm-client-site-server.exe",
"plat": "windows",
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_gen_win_exe.assert_called_with(
client=self.site2.client.pk,
site=self.site2.pk,
agent_type="server",
rdp=1,
ping=0,
power=0,
goarch="amd64",
token="token",
api="https://api.example.com",
file_name="rmm-client-site-server.exe",
)
# test linux no code sign
data["plat"] = "linux"
data["installMethod"] = "bash"
data["rdp"] = 0
data["agenttype"] = "workstation"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
# test linux
mock_token_valid.return_value = "token123", True
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_linux_install.assert_called_with(
client=str(self.site2.client.pk),
site=str(self.site2.pk),
agent_type="workstation",
arch="amd64",
token="token",
api="https://api.example.com",
download_url="https://example.com",
)
# test manual
data["rdp"] = 1
data["installMethod"] = "manual"
r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"])
self.assertNotIn("power", r.json()["cmd"])
data.update({"ping": 1, "power": 1})
r = self.client.post(url, data, format="json")
self.assertIn("power", r.json()["cmd"])
self.assertIn("ping", r.json()["cmd"])
# test powershell
data["installMethod"] = "powershell"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)

View File

@@ -1,313 +0,0 @@
from unittest.mock import patch
from django.conf import settings
from django.core.management import call_command
from model_bakery import baker
from packaging import version as pyver
from agents.models import Agent
from agents.tasks import auto_self_agent_update_task, send_agent_update_task
from logs.models import PendingAction
from tacticalrmm.constants import (
AGENT_DEFER,
AgentMonType,
AgentPlat,
GoArch,
PAAction,
PAStatus,
)
from tacticalrmm.test import TacticalTestCase
class TestAgentUpdate(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("agents.management.commands.update_agents.send_agent_update_task.delay")
@patch("agents.management.commands.update_agents.token_is_valid")
@patch("agents.management.commands.update_agents.get_core_settings")
def test_update_agents_mgmt_command(self, mock_core, mock_token, mock_update):
mock_token.return_value = ("token123", True)
baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.0.3",
_quantity=6,
)
baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.0.3",
_quantity=5,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version=settings.LATEST_AGENT_VER,
_quantity=8,
)
mock_core.return_value.agent_auto_update = False
call_command("update_agents")
mock_update.assert_not_called()
mock_core.return_value.agent_auto_update = True
call_command("update_agents")
ids = list(
Agent.objects.defer(*AGENT_DEFER)
.exclude(version=settings.LATEST_AGENT_VER)
.values_list("agent_id", flat=True)
)
mock_update.assert_called_with(agent_ids=ids, token="token123", force=False)
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.get_agent_url")
def test_do_update(self, mock_agent_url, mock_nats_cmd):
mock_agent_url.return_value = "https://example.com/123"
# test noarch
agent_noarch = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
)
r = agent_noarch.do_update(token="", force=True)
self.assertEqual(r, "noarch")
# test too old
agent_old = baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="1.3.0",
goarch=GoArch.AMD64,
)
r = agent_old.do_update(token="", force=True)
self.assertEqual(r, "not supported")
win = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
goarch=GoArch.AMD64,
)
lin = baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.3.0",
goarch=GoArch.ARM32,
)
# test windows agent update
r = win.do_update(token="", force=False)
self.assertEqual(r, "created")
mock_nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://example.com/123",
"version": settings.LATEST_AGENT_VER,
"inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
},
},
wait=False,
)
action1 = PendingAction.objects.get(agent__agent_id=win.agent_id)
self.assertEqual(action1.action_type, PAAction.AGENT_UPDATE)
self.assertEqual(action1.status, PAStatus.PENDING)
self.assertEqual(action1.details["url"], "https://example.com/123")
self.assertEqual(
action1.details["inno"],
f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
)
self.assertEqual(action1.details["version"], settings.LATEST_AGENT_VER)
mock_nats_cmd.reset_mock()
# test linux agent update
r = lin.do_update(token="", force=False)
mock_nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://example.com/123",
"version": settings.LATEST_AGENT_VER,
"inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
},
},
wait=False,
)
action2 = PendingAction.objects.get(agent__agent_id=lin.agent_id)
self.assertEqual(action2.action_type, PAAction.AGENT_UPDATE)
self.assertEqual(action2.status, PAStatus.PENDING)
self.assertEqual(action2.details["url"], "https://example.com/123")
self.assertEqual(
action2.details["inno"],
f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
)
self.assertEqual(action2.details["version"], settings.LATEST_AGENT_VER)
# check if old agent update pending actions are being deleted
# should only be 1 pending action at all times
pa_count = win.pendingactions.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).count()
self.assertEqual(pa_count, 1)
for _ in range(4):
win.do_update(token="", force=False)
pa_count = win.pendingactions.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).count()
self.assertEqual(pa_count, 1)
def test_auto_self_agent_update_task(self):
auto_self_agent_update_task()
@patch("agents.models.Agent.do_update")
def test_send_agent_update_task(self, mock_update):
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
goarch=GoArch.AMD64,
_quantity=6,
)
ids = list(
Agent.objects.defer(*AGENT_DEFER)
.exclude(version=settings.LATEST_AGENT_VER)
.values_list("agent_id", flat=True)
)
send_agent_update_task(agent_ids=ids, token="", force=False)
self.assertEqual(mock_update.call_count, 6)
@patch("agents.views.token_is_valid")
@patch("agents.tasks.send_agent_update_task.delay")
def test_update_agents(self, mock_update, mock_token):
mock_token.return_value = ("", False)
url = "/agents/update/"
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
goarch=GoArch.AMD64,
_quantity=7,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version=settings.LATEST_AGENT_VER,
goarch=GoArch.AMD64,
_quantity=3,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.0.1",
goarch=GoArch.ARM32,
_quantity=9,
)
agent_ids: list[str] = list(
Agent.objects.only("agent_id").values_list("agent_id", flat=True)
)
data = {"agent_ids": agent_ids}
expected: list[str] = [
i.agent_id
for i in Agent.objects.only("agent_id", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_update.assert_called_with(agent_ids=expected, token="", force=False)
self.check_not_authenticated("post", url)
@patch("agents.views.token_is_valid")
@patch("agents.tasks.send_agent_update_task.delay")
def test_agent_update_permissions(self, update_task, mock_token):
mock_token.return_value = ("", False)
agents = baker.make_recipe("agents.agent", _quantity=5)
other_agents = baker.make_recipe("agents.agent", _quantity=7)
url = f"/agents/update/"
data = {
"agent_ids": [agent.agent_id for agent in agents]
+ [agent.agent_id for agent in other_agents]
}
# test superuser access
self.check_authorized_superuser("post", url, data)
update_task.assert_called_with(
agent_ids=data["agent_ids"], token="", force=False
)
update_task.reset_mock()
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
self.check_not_authorized("post", url, data)
update_task.assert_not_called()
user.role.can_update_agents = True
user.role.save()
self.check_authorized("post", url, data)
update_task.assert_called_with(
agent_ids=data["agent_ids"], token="", force=False
)
update_task.reset_mock()
# limit to client
# user.role.can_view_clients.set([agents[0].client])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents])
# update_task.reset_mock()
# add site
# user.role.can_view_sites.set([other_agents[0].site])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=data["agent_ids"])
# update_task.reset_mock()
# remove client permissions
# user.role.can_view_clients.clear()
# self.check_authorized("post", url, data)
# update_task.assert_called_with(
# agent_ids=[agent.agent_id for agent in other_agents]
# )

View File

@@ -1,60 +0,0 @@
from unittest.mock import patch, AsyncMock
from django.conf import settings
from rest_framework.response import Response
from agents.utils import generate_linux_install, get_agent_url
from tacticalrmm.test import TacticalTestCase
class TestAgentUtils(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
def test_get_agent_url(self):
ver = settings.LATEST_AGENT_VER
# test without token
r = get_agent_url(goarch="amd64", plat="windows", token="")
expected = f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-windows-amd64.exe"
self.assertEqual(r, expected)
# test with token
r = get_agent_url(goarch="386", plat="linux", token="token123")
expected = f"https://{settings.AGENTS_URL}version={ver}&arch=386&token=token123&plat=linux&api=api.example.com"
@patch("agents.utils.get_mesh_device_id")
@patch("agents.utils.asyncio.run")
@patch("agents.utils.get_mesh_ws_url")
@patch("agents.utils.get_core_settings")
def test_generate_linux_install(
self, mock_core, mock_mesh, mock_async_run, mock_mesh_device_id
):
mock_mesh_device_id.return_value = "meshdeviceid"
mock_core.return_value.mesh_site = "meshsite"
mock_async_run.return_value = "meshid"
mock_mesh.return_value = "meshws"
r = generate_linux_install(
client="1",
site="1",
agent_type="server",
arch="amd64",
token="token123",
api="api.example.com",
download_url="asdasd3423",
)
ret = r.getvalue().decode("utf-8")
self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn(
r"meshDL='meshsite/meshagents?id=meshid&installflags=0&meshinstall=6'", ret
)
self.assertIn(r"apiURL='api.example.com'", ret)
self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn(r"token='token123'", ret)
self.assertIn(r"clientID='1'", ret)
self.assertIn(r"siteID='1'", ret)
self.assertIn(r"agentType='server'", ret)

View File

@@ -1,46 +0,0 @@
from unittest.mock import call, patch
from django.core.management import call_command
from model_bakery import baker
from tacticalrmm.constants import AgentMonType, AgentPlat
from tacticalrmm.test import TacticalTestCase
class TestBulkRestartAgents(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("core.management.commands.bulk_restart_agents.sleep")
@patch("agents.models.Agent.recover")
@patch("core.management.commands.bulk_restart_agents.get_mesh_ws_url")
def test_bulk_restart_agents_mgmt_cmd(
self, get_mesh_ws_url, recover, mock_sleep
) -> None:
get_mesh_ws_url.return_value = "https://mesh.example.com/test"
baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
)
baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.LINUX,
)
calls = [
call("tacagent", "https://mesh.example.com/test", wait=False),
call("mesh", "", wait=False),
]
call_command("bulk_restart_agents")
recover.assert_has_calls(calls)
mock_sleep.assert_called_with(10)

View File

@@ -1,63 +0,0 @@
from typing import TYPE_CHECKING
from unittest.mock import patch
from model_bakery import baker
from tacticalrmm.constants import AgentMonType, AgentPlat
from tacticalrmm.test import TacticalTestCase
if TYPE_CHECKING:
from clients.models import Client, Site
class TestRecovery(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.client1: "Client" = baker.make("clients.Client")
self.site1: "Site" = baker.make("clients.Site", client=self.client1)
@patch("agents.models.Agent.recover")
@patch("agents.views.get_mesh_ws_url")
def test_recover(self, get_mesh_ws_url, recover) -> None:
get_mesh_ws_url.return_value = "https://mesh.example.com"
agent = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
)
url = f"/agents/{agent.agent_id}/recover/"
# test successfull tacticalagent recovery
data = {"mode": "tacagent"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
recover.assert_called_with("tacagent", "https://mesh.example.com", wait=False)
get_mesh_ws_url.assert_called_once()
# reset mocks
recover.reset_mock()
get_mesh_ws_url.reset_mock()
# test successfull mesh agent recovery
data = {"mode": "mesh"}
recover.return_value = ("ok", False)
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
get_mesh_ws_url.assert_not_called()
recover.assert_called_with("mesh", "")
# reset mocks
recover.reset_mock()
get_mesh_ws_url.reset_mock()
# test failed mesh agent recovery
data = {"mode": "mesh"}
recover.return_value = ("Unable to contact the agent", True)
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)

View File

@@ -1,10 +1,9 @@
from django.urls import path from django.urls import path
from autotasks.views import GetAddAutoTasks
from checks.views import GetAddChecks
from logs.views import PendingActions
from . import views from . import views
from checks.views import GetAddChecks
from autotasks.views import GetAddAutoTasks
from logs.views import PendingActions
urlpatterns = [ urlpatterns = [
# agent views # agent views
@@ -41,5 +40,5 @@ urlpatterns = [
path("versions/", views.get_agent_versions), path("versions/", views.get_agent_versions),
path("update/", views.update_agents), path("update/", views.update_agents),
path("installer/", views.install_agent), path("installer/", views.install_agent),
path("bulkrecovery/", views.bulk_agent_recovery), path("<str:arch>/getmeshexe/", views.get_mesh_exe),
] ]

View File

@@ -1,81 +1,40 @@
import asyncio import random
import tempfile
import urllib.parse import urllib.parse
import requests
from django.conf import settings from django.conf import settings
from django.http import FileResponse from core.models import CodeSignToken
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
from tacticalrmm.constants import MeshAgentIdent
def get_agent_url(*, goarch: str, plat: str, token: str = "") -> str: def get_exegen_url() -> str:
ver = settings.LATEST_AGENT_VER urls: list[str] = settings.EXE_GEN_URLS
if token: for url in urls:
try:
r = requests.get(url, timeout=10)
except:
continue
if r.status_code == 200:
return url
return random.choice(urls)
def get_winagent_url(arch: str) -> str:
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
try:
t: CodeSignToken = CodeSignToken.objects.first() # type: ignore
if t.is_valid:
base_url = get_exegen_url() + "/api/v1/winagents/?"
params = { params = {
"version": ver, "version": settings.LATEST_AGENT_VER,
"arch": goarch, "arch": arch,
"token": token, "token": t.token,
"plat": plat,
"api": settings.ALLOWED_HOSTS[0],
} }
return settings.AGENTS_URL + urllib.parse.urlencode(params) dl_url = base_url + urllib.parse.urlencode(params)
except:
pass
return f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-{plat}-{goarch}.exe" return dl_url
def generate_linux_install(
client: str,
site: str,
agent_type: str,
arch: str,
token: str,
api: str,
download_url: str,
) -> FileResponse:
match arch:
case "amd64":
arch_id = MeshAgentIdent.LINUX64
case "386":
arch_id = MeshAgentIdent.LINUX32
case "arm64":
arch_id = MeshAgentIdent.LINUX_ARM_64
case "arm":
arch_id = MeshAgentIdent.LINUX_ARM_HF
case _:
arch_id = "not_found"
core = get_core_settings()
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_dl = (
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"
)
sh = settings.LINUX_AGENT_SCRIPT
with open(sh, "r") as f:
text = f.read()
replace = {
"agentDLChange": download_url,
"meshDLChange": mesh_dl,
"clientIDChange": client,
"siteIDChange": site,
"agentTypeChange": agent_type,
"tokenChange": token,
"apiURLChange": api,
}
for i, j in replace.items():
text = text.replace(i, j)
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
f.write(text)
f.write("\n")
return FileResponse(
open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh"
)

View File

@@ -6,102 +6,72 @@ import string
import time import time
from django.conf import settings from django.conf import settings
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime from django.db.models import Q
from meshctrl.utils import get_login_token
from packaging import version as pyver from packaging import version as pyver
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from core.utils import ( from core.models import CoreSettings
get_core_settings,
get_mesh_ws_url,
remove_mesh_agent,
token_is_valid,
)
from logs.models import AuditLog, DebugLog, PendingAction from logs.models import AuditLog, DebugLog, PendingAction
from scripts.models import Script from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.constants import ( from tacticalrmm.utils import (
get_default_timezone,
notify_error,
reload_nats,
AGENT_DEFER, AGENT_DEFER,
AGENT_STATUS_OFFLINE,
AGENT_STATUS_ONLINE,
AgentHistoryType,
AgentMonType,
AgentPlat,
CustomFieldModel,
EvtLogNames,
PAAction,
PAStatus,
) )
from tacticalrmm.helpers import notify_error from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from tacticalrmm.permissions import ( from tacticalrmm.permissions import (
_has_perm_on_agent, _has_perm_on_agent,
_has_perm_on_client, _has_perm_on_client,
_has_perm_on_site, _has_perm_on_site,
) )
from tacticalrmm.utils import get_default_timezone, reload_nats
from winupdate.models import WinUpdate
from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from .models import Agent, AgentCustomField, AgentHistory, Note from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
from .permissions import ( from .permissions import (
AgentHistoryPerms, AgentHistoryPerms,
AgentNotesPerms,
AgentPerms, AgentPerms,
EvtLogPerms, EvtLogPerms,
InstallAgentPerms, InstallAgentPerms,
RecoverAgentPerms,
AgentNotesPerms,
ManageProcPerms, ManageProcPerms,
MeshPerms, MeshPerms,
PingAgentPerms,
RebootAgentPerms, RebootAgentPerms,
RecoverAgentPerms,
RunBulkPerms, RunBulkPerms,
RunScriptPerms, RunScriptPerms,
SendCMDPerms, SendCMDPerms,
PingAgentPerms,
UpdateAgentPerms, UpdateAgentPerms,
) )
from .serializers import ( from .serializers import (
AgentCustomFieldSerializer, AgentCustomFieldSerializer,
AgentHistorySerializer, AgentHistorySerializer,
AgentHostnameSerializer, AgentHostnameSerializer,
AgentNoteSerializer,
AgentSerializer, AgentSerializer,
AgentTableSerializer, AgentTableSerializer,
AgentNoteSerializer,
) )
from .tasks import ( from .tasks import run_script_email_results_task, send_agent_update_task
bulk_recover_agents_task,
run_script_email_results_task,
send_agent_update_task,
)
class GetAgents(APIView): class GetAgents(APIView):
permission_classes = [IsAuthenticated, AgentPerms] permission_classes = [IsAuthenticated, AgentPerms]
def get(self, request): def get(self, request):
from checks.models import Check, CheckResult
monitoring_type_filter = Q()
client_site_filter = Q()
monitoring_type = request.query_params.get("monitoring_type", None)
if monitoring_type:
if monitoring_type in AgentMonType.values:
monitoring_type_filter = Q(monitoring_type=monitoring_type)
else:
return notify_error("monitoring type does not exist")
if "site" in request.query_params.keys(): if "site" in request.query_params.keys():
client_site_filter = Q(site_id=request.query_params["site"]) filter = Q(site_id=request.query_params["site"])
elif "client" in request.query_params.keys(): elif "client" in request.query_params.keys():
client_site_filter = Q(site__client_id=request.query_params["client"]) filter = Q(site__client_id=request.query_params["client"])
else:
filter = Q()
# by default detail=true # by default detail=true
if ( if (
@@ -109,53 +79,24 @@ class GetAgents(APIView):
or "detail" in request.query_params.keys() or "detail" in request.query_params.keys()
and request.query_params["detail"] == "true" and request.query_params["detail"] == "true"
): ):
agents = ( agents = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user) # type: ignore
.filter(monitoring_type_filter) .select_related("site", "policy", "alert_template")
.filter(client_site_filter) .prefetch_related("agentchecks")
.filter(filter)
.defer(*AGENT_DEFER) .defer(*AGENT_DEFER)
.select_related(
"site__server_policy",
"site__workstation_policy",
"site__client__server_policy",
"site__client__workstation_policy",
"policy",
"alert_template",
) )
.prefetch_related( ctx = {"default_tz": get_default_timezone()}
Prefetch( serializer = AgentTableSerializer(agents, many=True, context=ctx)
"agentchecks",
queryset=Check.objects.select_related("script"),
),
Prefetch(
"checkresults",
queryset=CheckResult.objects.select_related("assigned_check"),
),
)
.annotate(
pending_actions_count=Count(
"pendingactions",
filter=Q(pendingactions__status=PAStatus.PENDING),
)
)
.annotate(
has_patches_pending=Exists(
WinUpdate.objects.filter(
agent_id=OuterRef("pk"), action="approve", installed=False
)
)
)
)
serializer = AgentTableSerializer(agents, many=True)
# if detail=false # if detail=false
else: else:
agents = ( agents = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user) # type: ignore
.defer(*AGENT_DEFER) .select_related("site")
.select_related("site__client") .filter(filter)
.filter(monitoring_type_filter) .only("agent_id", "hostname", "site")
.filter(client_site_filter)
) )
serializer = AgentHostnameSerializer(agents, many=True) serializer = AgentHostnameSerializer(agents, many=True)
@@ -191,13 +132,13 @@ class GetUpdateDeleteAgent(APIView):
for field in request.data["custom_fields"]: for field in request.data["custom_fields"]:
custom_field = field custom_field = field
custom_field["agent"] = agent.pk custom_field["agent"] = agent.id # type: ignore
if AgentCustomField.objects.filter( if AgentCustomField.objects.filter(
field=field["field"], agent=agent.pk field=field["field"], agent=agent.id # type: ignore
): ):
value = AgentCustomField.objects.get( value = AgentCustomField.objects.get(
field=field["field"], agent=agent.pk field=field["field"], agent=agent.id # type: ignore
) )
serializer = AgentCustomFieldSerializer( serializer = AgentCustomFieldSerializer(
instance=value, data=custom_field instance=value, data=custom_field
@@ -214,19 +155,10 @@ class GetUpdateDeleteAgent(APIView):
# uninstall agent # uninstall agent
def delete(self, request, agent_id): def delete(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
code = "foo"
if agent.plat == AgentPlat.LINUX:
with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
code = f.read()
asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
name = agent.hostname name = agent.hostname
mesh_id = agent.mesh_node_id
agent.delete() agent.delete()
reload_nats() reload_nats()
uri = get_mesh_ws_url()
asyncio.run(remove_mesh_agent(uri, mesh_id))
return Response(f"{name} will now be uninstalled.") return Response(f"{name} will now be uninstalled.")
@@ -267,19 +199,19 @@ class AgentMeshCentral(APIView):
# get mesh urls # get mesh urls
def get(self, request, agent_id): def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
core = get_core_settings() core = CoreSettings.objects.first()
if not core.mesh_disable_auto_login: token = agent.get_login_token(
token = get_login_token( key=core.mesh_token,
key=core.mesh_token, user=f"user//{core.mesh_username}" user=f"user//{core.mesh_username.lower()}", # type:ignore
) )
token_param = f"login={token}&"
else:
token_param = ""
control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31" if token == "err":
terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31" return notify_error("Invalid mesh token")
file = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=13&hide=31"
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
AuditLog.audit_mesh_session( AuditLog.audit_mesh_session(
username=request.user.username, username=request.user.username,
@@ -313,9 +245,9 @@ class AgentMeshCentral(APIView):
@permission_classes([IsAuthenticated, AgentPerms]) @permission_classes([IsAuthenticated, AgentPerms])
def get_agent_versions(request): def get_agent_versions(request):
agents = ( agents = (
Agent.objects.defer(*AGENT_DEFER) Agent.objects.filter_by_role(request.user)
.filter_by_role(request.user) # type: ignore .prefetch_related("site")
.select_related("site__client") .only("pk", "hostname")
) )
return Response( return Response(
{ {
@@ -329,7 +261,7 @@ def get_agent_versions(request):
@permission_classes([IsAuthenticated, UpdateAgentPerms]) @permission_classes([IsAuthenticated, UpdateAgentPerms])
def update_agents(request): def update_agents(request):
q = ( q = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.filter(agent_id__in=request.data["agent_ids"]) .filter(agent_id__in=request.data["agent_ids"])
.only("agent_id", "version") .only("agent_id", "version")
) )
@@ -338,9 +270,7 @@ def update_agents(request):
for i in q for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
] ]
send_agent_update_task.delay(agent_ids=agent_ids)
token, _ = token_is_valid()
send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False)
return Response("ok") return Response("ok")
@@ -348,18 +278,18 @@ def update_agents(request):
@permission_classes([IsAuthenticated, PingAgentPerms]) @permission_classes([IsAuthenticated, PingAgentPerms])
def ping(request, agent_id): def ping(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
status = AGENT_STATUS_OFFLINE status = "offline"
attempts = 0 attempts = 0
while 1: while 1:
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong": if r == "pong":
status = AGENT_STATUS_ONLINE status = "online"
break break
else: else:
attempts += 1 attempts += 1
time.sleep(0.5) time.sleep(1)
if attempts >= 3: if attempts >= 5:
break break
return Response({"name": agent.hostname, "status": status}) return Response({"name": agent.hostname, "status": status})
@@ -374,7 +304,7 @@ def get_event_log(request, agent_id, logtype, days):
return demo_get_eventlog() return demo_get_eventlog()
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
timeout = 180 if logtype == EvtLogNames.SECURITY else 30 timeout = 180 if logtype == "Security" else 30
data = { data = {
"func": "eventlog", "func": "eventlog",
@@ -396,23 +326,18 @@ def get_event_log(request, agent_id, logtype, days):
def send_raw_cmd(request, agent_id): def send_raw_cmd(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
timeout = int(request.data["timeout"]) timeout = int(request.data["timeout"])
if request.data["shell"] == "custom" and request.data["custom_shell"]:
shell = request.data["custom_shell"]
else:
shell = request.data["shell"]
data = { data = {
"func": "rawcmd", "func": "rawcmd",
"timeout": timeout, "timeout": timeout,
"payload": { "payload": {
"command": request.data["cmd"], "command": request.data["cmd"],
"shell": shell, "shell": request.data["shell"],
}, },
} }
hist = AgentHistory.objects.create( hist = AgentHistory.objects.create(
agent=agent, agent=agent,
type=AgentHistoryType.CMD_RUN, type="cmd_run",
command=request.data["cmd"], command=request.data["cmd"],
username=request.user.username[:50], username=request.user.username[:50],
) )
@@ -427,7 +352,7 @@ def send_raw_cmd(request, agent_id):
username=request.user.username, username=request.user.username,
agent=agent, agent=agent,
cmd=request.data["cmd"], cmd=request.data["cmd"],
shell=shell, shell=request.data["shell"],
debug_info={"ip": request._client_ip}, debug_info={"ip": request._client_ip},
) )
@@ -448,11 +373,9 @@ class Reboot(APIView):
# reboot later # reboot later
def patch(self, request, agent_id): def patch(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
if agent.is_posix:
return notify_error(f"Not currently implemented for {agent.plat}")
try: try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S") obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
except Exception: except Exception:
return notify_error("Invalid date") return notify_error("Invalid date")
@@ -460,28 +383,18 @@ class Reboot(APIView):
random.choice(string.ascii_letters) for _ in range(10) random.choice(string.ascii_letters) for _ in range(10)
) )
expire_date = obj + djangotime.timedelta(minutes=5)
nats_data = { nats_data = {
"func": "schedtask", "func": "schedtask",
"schedtaskpayload": { "schedtaskpayload": {
"type": "schedreboot", "type": "schedreboot",
"enabled": True, "deleteafter": True,
"delete_expired_task_after": True, "trigger": "once",
"start_when_available": False,
"multiple_instances": 2,
"trigger": "runonce",
"name": task_name, "name": task_name,
"start_year": int(dt.datetime.strftime(obj, "%Y")), "year": int(dt.datetime.strftime(obj, "%Y")),
"start_month": int(dt.datetime.strftime(obj, "%-m")), "month": dt.datetime.strftime(obj, "%B"),
"start_day": int(dt.datetime.strftime(obj, "%-d")), "day": int(dt.datetime.strftime(obj, "%d")),
"start_hour": int(dt.datetime.strftime(obj, "%-H")), "hour": int(dt.datetime.strftime(obj, "%H")),
"start_min": int(dt.datetime.strftime(obj, "%-M")), "min": int(dt.datetime.strftime(obj, "%M")),
"expire_year": int(expire_date.strftime("%Y")),
"expire_month": int(expire_date.strftime("%-m")),
"expire_day": int(expire_date.strftime("%-d")),
"expire_hour": int(expire_date.strftime("%-H")),
"expire_min": int(expire_date.strftime("%-M")),
}, },
} }
@@ -491,7 +404,7 @@ class Reboot(APIView):
details = {"taskname": task_name, "time": str(obj)} details = {"taskname": task_name, "time": str(obj)}
PendingAction.objects.create( PendingAction.objects.create(
agent=agent, action_type=PAAction.SCHED_REBOOT, details=details agent=agent, action_type="schedreboot", details=details
) )
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
return Response( return Response(
@@ -503,24 +416,38 @@ class Reboot(APIView):
@permission_classes([IsAuthenticated, InstallAgentPerms]) @permission_classes([IsAuthenticated, InstallAgentPerms])
def install_agent(request): def install_agent(request):
from knox.models import AuthToken from knox.models import AuthToken
from accounts.models import User from accounts.models import User
from agents.utils import get_agent_url
from core.utils import token_is_valid from agents.utils import get_winagent_url
client_id = request.data["client"] client_id = request.data["client"]
site_id = request.data["site"] site_id = request.data["site"]
version = settings.LATEST_AGENT_VER version = settings.LATEST_AGENT_VER
goarch = request.data["goarch"] arch = request.data["arch"]
plat = request.data["plat"]
if not _has_perm_on_site(request.user, site_id): if not _has_perm_on_site(request.user, site_id):
raise PermissionDenied() raise PermissionDenied()
codesign_token, is_valid = token_is_valid() # response type is blob so we have to use
# status codes and render error message on the frontend
if arch == "64" and not os.path.exists(
os.path.join(settings.EXE_DIR, "meshagent.exe")
):
return notify_error(
"Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
inno = f"tacticalagent-v{version}-{plat}-{goarch}.exe" if arch == "32" and not os.path.exists(
download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token) os.path.join(settings.EXE_DIR, "meshagent-x86.exe")
):
return notify_error(
"Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
inno = (
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
)
download_url = get_winagent_url(arch)
installer_user = User.objects.filter(is_installer_user=True).first() installer_user = User.objects.filter(is_installer_user=True).first()
@@ -538,34 +465,12 @@ def install_agent(request):
rdp=request.data["rdp"], rdp=request.data["rdp"],
ping=request.data["ping"], ping=request.data["ping"],
power=request.data["power"], power=request.data["power"],
goarch=goarch, arch=arch,
token=token, token=token,
api=request.data["api"], api=request.data["api"],
file_name=request.data["fileName"], file_name=request.data["fileName"],
) )
elif request.data["installMethod"] == "bash":
# TODO
# linux agents are in beta for now, only available for sponsors for testing
# remove this after it's out of beta
if not is_valid:
return notify_error(
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
)
from agents.utils import generate_linux_install
return generate_linux_install(
client=str(client_id),
site=str(site_id),
agent_type=request.data["agenttype"],
arch=goarch,
token=token,
api=request.data["api"],
download_url=download_url,
)
elif request.data["installMethod"] == "manual": elif request.data["installMethod"] == "manual":
cmd = [ cmd = [
inno, inno,
@@ -655,24 +560,41 @@ def install_agent(request):
@api_view(["POST"]) @api_view(["POST"])
@permission_classes([IsAuthenticated, RecoverAgentPerms]) @permission_classes([IsAuthenticated, RecoverAgentPerms])
def recover(request, agent_id: str) -> Response: def recover(request, agent_id):
agent: Agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=agent_id)
Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
)
mode = request.data["mode"] mode = request.data["mode"]
if mode == "tacagent": # attempt a realtime recovery, otherwise fall back to old recovery method
uri = get_mesh_ws_url() if mode == "tacagent" or mode == "mesh":
agent.recover(mode, uri, wait=False) data = {"func": "recover", "payload": {"mode": mode}}
return Response("Recovery will be attempted shortly") r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
elif mode == "mesh":
r, err = agent.recover(mode, "")
if err:
return notify_error(f"Unable to complete recovery: {r}")
return Response("Successfully completed recovery") return Response("Successfully completed recovery")
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
return notify_error(
"A recovery action is currently pending. Please wait for the next agent check-in."
)
if mode == "command" and not request.data["cmd"]:
return notify_error("Command is required")
# if we've made it this far and realtime recovery didn't work,
# tacagent service is the fallback recovery so we obv can't use that to recover itself if it's down
if mode == "tacagent":
return notify_error(
"Requires RPC service to be functional. Please recover that first"
)
# we should only get here if all other methods fail
RecoveryAction(
agent=agent,
mode=mode,
command=request.data["cmd"] if mode == "command" else None,
).save()
return Response("Recovery will be attempted on the agent's next check-in")
@api_view(["POST"]) @api_view(["POST"])
@permission_classes([IsAuthenticated, RunScriptPerms]) @permission_classes([IsAuthenticated, RunScriptPerms])
@@ -692,7 +614,7 @@ def run_script(request, agent_id):
hist = AgentHistory.objects.create( hist = AgentHistory.objects.create(
agent=agent, agent=agent,
type=AgentHistoryType.SCRIPT_RUN, type="script_run",
script=script, script=script,
username=request.user.username[:50], username=request.user.username[:50],
) )
@@ -732,11 +654,11 @@ def run_script(request, agent_id):
custom_field = CustomField.objects.get(pk=request.data["custom_field"]) custom_field = CustomField.objects.get(pk=request.data["custom_field"])
if custom_field.model == CustomFieldModel.AGENT: if custom_field.model == "agent":
field = custom_field.get_or_create_field_value(agent) field = custom_field.get_or_create_field_value(agent)
elif custom_field.model == CustomFieldModel.CLIENT: elif custom_field.model == "client":
field = custom_field.get_or_create_field_value(agent.client) field = custom_field.get_or_create_field_value(agent.client)
elif custom_field.model == CustomFieldModel.SITE: elif custom_field.model == "site":
field = custom_field.get_or_create_field_value(agent.site) field = custom_field.get_or_create_field_value(agent.site)
else: else:
return notify_error("Custom Field was invalid") return notify_error("Custom Field was invalid")
@@ -768,6 +690,27 @@ def run_script(request, agent_id):
return Response(f"{script.name} will now be run on {agent.hostname}") return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view(["POST"])
def get_mesh_exe(request, arch):
filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
mesh_exe = os.path.join(settings.EXE_DIR, filename)
if not os.path.exists(mesh_exe):
return notify_error(f"File {filename} has not been uploaded.")
if settings.DEBUG:
with open(mesh_exe, "rb") as f:
response = HttpResponse(
f.read(), content_type="application/vnd.microsoft.portable-executable"
)
response["Content-Disposition"] = f"inline; filename={filename}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={filename}"
response["X-Accel-Redirect"] = f"/private/exe/{filename}"
return response
class GetAddNotes(APIView): class GetAddNotes(APIView):
permission_classes = [IsAuthenticated, AgentNotesPerms] permission_classes = [IsAuthenticated, AgentNotesPerms]
@@ -776,7 +719,7 @@ class GetAddNotes(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
notes = Note.objects.filter(agent=agent) notes = Note.objects.filter(agent=agent)
else: else:
notes = Note.objects.filter_by_role(request.user) # type: ignore notes = Note.objects.filter_by_role(request.user)
return Response(AgentNoteSerializer(notes, many=True).data) return Response(AgentNoteSerializer(notes, many=True).data)
@@ -785,9 +728,6 @@ class GetAddNotes(APIView):
if not _has_perm_on_agent(request.user, agent.agent_id): if not _has_perm_on_agent(request.user, agent.agent_id):
raise PermissionDenied() raise PermissionDenied()
if "note" not in request.data.keys():
return notify_error("Cannot add an empty note")
data = { data = {
"note": request.data["note"], "note": request.data["note"],
"agent": agent.pk, "agent": agent.pk,
@@ -841,37 +781,32 @@ def bulk(request):
if request.data["target"] == "client": if request.data["target"] == "client":
if not _has_perm_on_client(request.user, request.data["client"]): if not _has_perm_on_client(request.user, request.data["client"]):
raise PermissionDenied() raise PermissionDenied()
q = Agent.objects.filter_by_role(request.user).filter( # type: ignore q = Agent.objects.filter_by_role(request.user).filter(
site__client_id=request.data["client"] site__client_id=request.data["client"]
) )
elif request.data["target"] == "site": elif request.data["target"] == "site":
if not _has_perm_on_site(request.user, request.data["site"]): if not _has_perm_on_site(request.user, request.data["site"]):
raise PermissionDenied() raise PermissionDenied()
q = Agent.objects.filter_by_role(request.user).filter( # type: ignore q = Agent.objects.filter_by_role(request.user).filter(
site_id=request.data["site"] site_id=request.data["site"]
) )
elif request.data["target"] == "agents": elif request.data["target"] == "agents":
q = Agent.objects.filter_by_role(request.user).filter( # type: ignore q = Agent.objects.filter_by_role(request.user).filter(
agent_id__in=request.data["agents"] agent_id__in=request.data["agents"]
) )
elif request.data["target"] == "all": elif request.data["target"] == "all":
q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type") # type: ignore q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type")
else: else:
return notify_error("Something went wrong") return notify_error("Something went wrong")
if request.data["monType"] == "servers": if request.data["monType"] == "servers":
q = q.filter(monitoring_type=AgentMonType.SERVER) q = q.filter(monitoring_type="server")
elif request.data["monType"] == "workstations": elif request.data["monType"] == "workstations":
q = q.filter(monitoring_type=AgentMonType.WORKSTATION) q = q.filter(monitoring_type="workstation")
if request.data["osType"] == AgentPlat.WINDOWS:
q = q.filter(plat=AgentPlat.WINDOWS)
elif request.data["osType"] == AgentPlat.LINUX:
q = q.filter(plat=AgentPlat.LINUX)
agents: list[int] = [agent.pk for agent in q] agents: list[int] = [agent.pk for agent in q]
@@ -886,15 +821,10 @@ def bulk(request):
) )
if request.data["mode"] == "command": if request.data["mode"] == "command":
if request.data["shell"] == "custom" and request.data["custom_shell"]:
shell = request.data["custom_shell"]
else:
shell = request.data["shell"]
handle_bulk_command_task.delay( handle_bulk_command_task.delay(
agents, agents,
request.data["cmd"], request.data["cmd"],
shell, request.data["shell"],
request.data["timeout"], request.data["timeout"],
request.user.username[:50], request.user.username[:50],
run_on_offline=request.data["offlineAgents"], run_on_offline=request.data["offlineAgents"],
@@ -935,7 +865,7 @@ def agent_maintenance(request):
raise PermissionDenied() raise PermissionDenied()
count = ( count = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.filter(site__client_id=request.data["id"]) .filter(site__client_id=request.data["id"])
.update(maintenance_mode=request.data["action"]) .update(maintenance_mode=request.data["action"])
) )
@@ -945,7 +875,7 @@ def agent_maintenance(request):
raise PermissionDenied() raise PermissionDenied()
count = ( count = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.filter(site_id=request.data["id"]) .filter(site_id=request.data["id"])
.update(maintenance_mode=request.data["action"]) .update(maintenance_mode=request.data["action"])
) )
@@ -962,13 +892,6 @@ def agent_maintenance(request):
) )
@api_view(["GET"])
@permission_classes([IsAuthenticated, RecoverAgentPerms])
def bulk_agent_recovery(request):
bulk_recover_agents_task.delay()
return Response("Agents will now be recovered")
class WMI(APIView): class WMI(APIView):
permission_classes = [IsAuthenticated, AgentPerms] permission_classes = [IsAuthenticated, AgentPerms]
@@ -988,6 +911,6 @@ class AgentHistoryView(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
history = AgentHistory.objects.filter(agent=agent) history = AgentHistory.objects.filter(agent=agent)
else: else:
history = AgentHistory.objects.filter_by_role(request.user) # type: ignore history = AgentHistory.objects.filter_by_role(request.user)
ctx = {"default_tz": get_default_timezone()} ctx = {"default_tz": get_default_timezone()}
return Response(AgentHistorySerializer(history, many=True, context=ctx).data) return Response(AgentHistorySerializer(history, many=True, context=ctx).data)

View File

@@ -1,24 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-07 17:28
import django.db.models.deletion
from django.db import migrations, models
def delete_alerts_without_agent(apps, schema):
Alert = apps.get_model("alerts", "Alert")
Alert.objects.filter(agent=None).delete()
class Migration(migrations.Migration):
dependencies = [
("agents", "0047_alter_agent_plat_alter_agent_site"),
("alerts", "0010_auto_20210917_1954"),
]
operations = [
migrations.RunPython(
delete_alerts_without_agent, reverse_code=migrations.RunPython.noop
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.0.5 on 2022-06-29 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0011_alter_alert_agent'),
]
operations = [
migrations.AlterField(
model_name='alert',
name='action_retcode',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='alert',
name='resolved_action_retcode',
field=models.BigIntegerField(blank=True, null=True),
),
]

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast from typing import TYPE_CHECKING, Union
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
@@ -9,20 +9,26 @@ from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from logs.models import BaseAuditModel, DebugLog from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.constants import (
AgentMonType,
AlertSeverity,
AlertType,
CheckType,
DebugLogType,
)
from tacticalrmm.models import PermissionQuerySet from tacticalrmm.models import PermissionQuerySet
if TYPE_CHECKING: if TYPE_CHECKING:
from agents.models import Agent from agents.models import Agent
from autotasks.models import AutomatedTask, TaskResult from autotasks.models import AutomatedTask
from checks.models import Check, CheckResult from checks.models import Check
from clients.models import Client, Site
SEVERITY_CHOICES = [
("info", "Informational"),
("warning", "Warning"),
("error", "Error"),
]
ALERT_TYPE_CHOICES = [
("availability", "Availability"),
("check", "Check"),
("task", "Task"),
("custom", "Custom"),
]
class Alert(models.Model): class Alert(models.Model):
@@ -50,7 +56,7 @@ class Alert(models.Model):
blank=True, blank=True,
) )
alert_type = models.CharField( alert_type = models.CharField(
max_length=20, choices=AlertType.choices, default=AlertType.AVAILABILITY max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
) )
message = models.TextField(null=True, blank=True) message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
@@ -58,9 +64,7 @@ class Alert(models.Model):
snooze_until = models.DateTimeField(null=True, blank=True) snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False) resolved = models.BooleanField(default=False)
resolved_on = models.DateTimeField(null=True, blank=True) resolved_on = models.DateTimeField(null=True, blank=True)
severity = models.CharField( severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO
)
email_sent = models.DateTimeField(null=True, blank=True) email_sent = models.DateTimeField(null=True, blank=True)
resolved_email_sent = models.DateTimeField(null=True, blank=True) resolved_email_sent = models.DateTimeField(null=True, blank=True)
sms_sent = models.DateTimeField(null=True, blank=True) sms_sent = models.DateTimeField(null=True, blank=True)
@@ -69,208 +73,72 @@ class Alert(models.Model):
action_run = models.DateTimeField(null=True, blank=True) action_run = models.DateTimeField(null=True, blank=True)
action_stdout = models.TextField(null=True, blank=True) action_stdout = models.TextField(null=True, blank=True)
action_stderr = models.TextField(null=True, blank=True) action_stderr = models.TextField(null=True, blank=True)
action_retcode = models.BigIntegerField(null=True, blank=True) action_retcode = models.IntegerField(null=True, blank=True)
action_execution_time = models.CharField(max_length=100, null=True, blank=True) action_execution_time = models.CharField(max_length=100, null=True, blank=True)
resolved_action_run = models.DateTimeField(null=True, blank=True) resolved_action_run = models.DateTimeField(null=True, blank=True)
resolved_action_stdout = models.TextField(null=True, blank=True) resolved_action_stdout = models.TextField(null=True, blank=True)
resolved_action_stderr = models.TextField(null=True, blank=True) resolved_action_stderr = models.TextField(null=True, blank=True)
resolved_action_retcode = models.BigIntegerField(null=True, blank=True) resolved_action_retcode = models.IntegerField(null=True, blank=True)
resolved_action_execution_time = models.CharField( resolved_action_execution_time = models.CharField(
max_length=100, null=True, blank=True max_length=100, null=True, blank=True
) )
def __str__(self) -> str: def __str__(self):
return f"{self.alert_type} - {self.message}" return self.message
@property def resolve(self):
def assigned_agent(self) -> "Optional[Agent]":
return self.agent
@property
def site(self) -> "Site":
return self.agent.site
@property
def client(self) -> "Client":
return self.agent.client
def resolve(self) -> None:
self.resolved = True self.resolved = True
self.resolved_on = djangotime.now() self.resolved_on = djangotime.now()
self.snoozed = False self.snoozed = False
self.snooze_until = None self.snooze_until = None
self.save(update_fields=["resolved", "resolved_on", "snoozed", "snooze_until"]) self.save()
@classmethod @classmethod
def create_or_return_availability_alert( def create_or_return_availability_alert(cls, agent):
cls, agent: Agent, skip_create: bool = False if not cls.objects.filter(agent=agent, resolved=False).exists():
) -> Optional[Alert]: return cls.objects.create(
if not cls.objects.filter(
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
).exists():
if skip_create:
return None
return cast(
Alert,
cls.objects.create(
agent=agent, agent=agent,
alert_type=AlertType.AVAILABILITY, alert_type="availability",
severity=AlertSeverity.ERROR, severity="error",
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.", message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
hidden=True, hidden=True,
),
) )
else: else:
try: return cls.objects.get(agent=agent, resolved=False)
return cast(
Alert,
cls.objects.get(
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
),
)
except cls.MultipleObjectsReturned:
alerts = cls.objects.filter(
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
)
last_alert = cast(Alert, alerts.last())
# cycle through other alerts and resolve
for alert in alerts:
if alert.id != last_alert.pk:
alert.resolve()
return last_alert
except cls.DoesNotExist:
return None
@classmethod @classmethod
def create_or_return_check_alert( def create_or_return_check_alert(cls, check):
cls,
check: "Check",
agent: "Agent",
alert_severity: Optional[str] = None,
skip_create: bool = False,
) -> "Optional[Alert]":
# need to pass agent if the check is a policy if not cls.objects.filter(assigned_check=check, resolved=False).exists():
if not cls.objects.filter( return cls.objects.create(
assigned_check=check, assigned_check=check,
agent=agent, alert_type="check",
resolved=False, severity=check.alert_severity,
).exists(): message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
if skip_create:
return None
return cast(
Alert,
cls.objects.create(
assigned_check=check,
agent=agent,
alert_type=AlertType.CHECK,
severity=check.alert_severity
if check.check_type
not in [
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
]
else alert_severity,
message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True, hidden=True,
),
) )
else: else:
try: return cls.objects.get(assigned_check=check, resolved=False)
return cast(
Alert,
cls.objects.get(
assigned_check=check,
agent=agent,
resolved=False,
),
)
except cls.MultipleObjectsReturned:
alerts = cls.objects.filter(
assigned_check=check,
agent=agent,
resolved=False,
)
last_alert = cast(Alert, alerts.last())
# cycle through other alerts and resolve
for alert in alerts:
if alert.id != last_alert.pk:
alert.resolve()
return last_alert
except cls.DoesNotExist:
return None
@classmethod @classmethod
def create_or_return_task_alert( def create_or_return_task_alert(cls, task):
cls,
task: "AutomatedTask",
agent: "Agent",
skip_create: bool = False,
) -> "Optional[Alert]":
if not cls.objects.filter( if not cls.objects.filter(assigned_task=task, resolved=False).exists():
return cls.objects.create(
assigned_task=task, assigned_task=task,
agent=agent, alert_type="task",
resolved=False,
).exists():
if skip_create:
return None
return cast(
Alert,
cls.objects.create(
assigned_task=task,
agent=agent,
alert_type=AlertType.TASK,
severity=task.alert_severity, severity=task.alert_severity,
message=f"{agent.hostname} has task: {task.name} that failed.", message=f"{task.agent.hostname} has task: {task.name} that failed.",
hidden=True, hidden=True,
),
) )
else: else:
try: return cls.objects.get(assigned_task=task, resolved=False)
return cast(
Alert,
cls.objects.get(
assigned_task=task,
agent=agent,
resolved=False,
),
)
except cls.MultipleObjectsReturned:
alerts = cls.objects.filter(
assigned_task=task,
agent=agent,
resolved=False,
)
last_alert = cast(Alert, alerts.last())
# cycle through other alerts and resolve
for alert in alerts:
if alert.id != last_alert.pk:
alert.resolve()
return last_alert
except cls.DoesNotExist:
return None
@classmethod @classmethod
def handle_alert_failure( def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
cls, instance: Union[Agent, TaskResult, CheckResult]
) -> None:
from agents.models import Agent from agents.models import Agent
from autotasks.models import TaskResult from autotasks.models import AutomatedTask
from checks.models import CheckResult from checks.models import Check
# set variables # set variables
dashboard_severities = None dashboard_severities = None
@@ -282,7 +150,6 @@ class Alert(models.Model):
alert_interval = None alert_interval = None
email_task = None email_task = None
text_task = None text_task = None
run_script_action = None
# check what the instance passed is # check what the instance passed is
if isinstance(instance, Agent): if isinstance(instance, Agent):
@@ -296,21 +163,30 @@ class Alert(models.Model):
dashboard_alert = instance.overdue_dashboard_alert dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.alert_template alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode maintenance_mode = instance.maintenance_mode
alert_severity = AlertSeverity.ERROR alert_severity = "error"
agent = instance agent = instance
dashboard_severities = [AlertSeverity.ERROR]
email_severities = [AlertSeverity.ERROR]
text_severities = [AlertSeverity.ERROR]
# set alert_template settings # set alert_template settings
if alert_template: if alert_template:
dashboard_severities = ["error"]
email_severities = ["error"]
text_severities = ["error"]
always_dashboard = alert_template.agent_always_alert always_dashboard = alert_template.agent_always_alert
always_email = alert_template.agent_always_email always_email = alert_template.agent_always_email
always_text = alert_template.agent_always_text always_text = alert_template.agent_always_text
alert_interval = alert_template.agent_periodic_alert_days alert_interval = alert_template.agent_periodic_alert_days
run_script_action = alert_template.agent_script_actions run_script_action = alert_template.agent_script_actions
elif isinstance(instance, CheckResult): if instance.should_create_alert(alert_template):
alert = cls.create_or_return_availability_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(agent=instance, resolved=False).exists():
alert = cls.objects.get(agent=instance, resolved=False)
else:
alert = None
elif isinstance(instance, Check):
from checks.tasks import ( from checks.tasks import (
handle_check_email_alert_task, handle_check_email_alert_task,
handle_check_sms_alert_task, handle_check_sms_alert_task,
@@ -319,98 +195,75 @@ class Alert(models.Model):
email_task = handle_check_email_alert_task email_task = handle_check_email_alert_task
text_task = handle_check_sms_alert_task text_task = handle_check_sms_alert_task
email_alert = instance.assigned_check.email_alert email_alert = instance.email_alert
text_alert = instance.assigned_check.text_alert text_alert = instance.text_alert
dashboard_alert = instance.assigned_check.dashboard_alert dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode maintenance_mode = instance.agent.maintenance_mode
alert_severity = ( alert_severity = instance.alert_severity
instance.assigned_check.alert_severity
if instance.assigned_check.check_type
not in [
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
]
else instance.alert_severity
)
agent = instance.agent agent = instance.agent
# set alert_template settings # set alert_template settings
if alert_template: if alert_template:
dashboard_severities = ( dashboard_severities = alert_template.check_dashboard_alert_severity
alert_template.check_dashboard_alert_severity email_severities = alert_template.check_email_alert_severity
if alert_template.check_dashboard_alert_severity text_severities = alert_template.check_text_alert_severity
else [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
AlertSeverity.INFO,
]
)
email_severities = (
alert_template.check_email_alert_severity
if alert_template.check_email_alert_severity
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
)
text_severities = (
alert_template.check_text_alert_severity
if alert_template.check_text_alert_severity
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
)
always_dashboard = alert_template.check_always_alert always_dashboard = alert_template.check_always_alert
always_email = alert_template.check_always_email always_email = alert_template.check_always_email
always_text = alert_template.check_always_text always_text = alert_template.check_always_text
alert_interval = alert_template.check_periodic_alert_days alert_interval = alert_template.check_periodic_alert_days
run_script_action = alert_template.check_script_actions run_script_action = alert_template.check_script_actions
elif isinstance(instance, TaskResult): if instance.should_create_alert(alert_template):
alert = cls.create_or_return_check_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_check=instance, resolved=False).exists():
alert = cls.objects.get(assigned_check=instance, resolved=False)
else:
alert = None
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
email_task = handle_task_email_alert email_task = handle_task_email_alert
text_task = handle_task_sms_alert text_task = handle_task_sms_alert
email_alert = instance.task.email_alert email_alert = instance.email_alert
text_alert = instance.task.text_alert text_alert = instance.text_alert
dashboard_alert = instance.task.dashboard_alert dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.task.alert_severity alert_severity = instance.alert_severity
agent = instance.agent agent = instance.agent
# set alert_template settings # set alert_template settings
if alert_template: if alert_template:
dashboard_severities = ( dashboard_severities = alert_template.task_dashboard_alert_severity
alert_template.task_dashboard_alert_severity email_severities = alert_template.task_email_alert_severity
if alert_template.task_dashboard_alert_severity text_severities = alert_template.task_text_alert_severity
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
)
email_severities = (
alert_template.task_email_alert_severity
if alert_template.task_email_alert_severity
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
)
text_severities = (
alert_template.task_text_alert_severity
if alert_template.task_text_alert_severity
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
)
always_dashboard = alert_template.task_always_alert always_dashboard = alert_template.task_always_alert
always_email = alert_template.task_always_email always_email = alert_template.task_always_email
always_text = alert_template.task_always_text always_text = alert_template.task_always_text
alert_interval = alert_template.task_periodic_alert_days alert_interval = alert_template.task_periodic_alert_days
run_script_action = alert_template.task_script_actions run_script_action = alert_template.task_script_actions
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_task_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_task=instance, resolved=False).exists():
alert = cls.objects.get(assigned_task=instance, resolved=False)
else:
alert = None
else: else:
return return
alert = instance.get_or_create_alert_if_needed(alert_template)
# return if agent is in maintenance mode # return if agent is in maintenance mode
if not alert or maintenance_mode: if maintenance_mode or not alert:
return return
# check if alert severity changed and update the alert # check if alert severity changed on check and update the alert
if alert_severity != alert.severity: if alert_severity != alert.severity:
alert.severity = alert_severity alert.severity = alert_severity
alert.save(update_fields=["severity"]) alert.save(update_fields=["severity"])
@@ -419,25 +272,19 @@ class Alert(models.Model):
if dashboard_alert or always_dashboard: if dashboard_alert or always_dashboard:
# check if alert template is set and specific severities are configured # check if alert template is set and specific severities are configured
if ( if alert_template and alert.severity not in dashboard_severities: # type: ignore
not alert_template pass
or alert_template else:
and dashboard_severities
and alert.severity in dashboard_severities
):
alert.hidden = False alert.hidden = False
alert.save(update_fields=["hidden"]) alert.save()
# send email if enabled # send email if enabled
if email_alert or always_email: if email_alert or always_email:
# check if alert template is set and specific severities are configured # check if alert template is set and specific severities are configured
if ( if alert_template and alert.severity not in email_severities: # type: ignore
not alert_template pass
or alert_template else:
and email_severities
and alert.severity in email_severities
):
email_task.delay( email_task.delay(
pk=alert.pk, pk=alert.pk,
alert_interval=alert_interval, alert_interval=alert_interval,
@@ -447,21 +294,13 @@ class Alert(models.Model):
if text_alert or always_text: if text_alert or always_text:
# check if alert template is set and specific severities are configured # check if alert template is set and specific severities are configured
if ( if alert_template and alert.severity not in text_severities: # type: ignore
not alert_template pass
or alert_template else:
and text_severities
and alert.severity in text_severities
):
text_task.delay(pk=alert.pk, alert_interval=alert_interval) text_task.delay(pk=alert.pk, alert_interval=alert_interval)
# check if any scripts should be run # check if any scripts should be run
if ( if alert_template and alert_template.action and run_script_action and not alert.action_run: # type: ignore
alert_template
and alert_template.action
and run_script_action
and not alert.action_run
):
r = agent.run_script( r = agent.run_script(
scriptpk=alert_template.action.pk, scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args), args=alert.parse_script_args(alert_template.action_args),
@@ -472,7 +311,7 @@ class Alert(models.Model):
) )
# command was successful # command was successful
if isinstance(r, dict): if type(r) == dict:
alert.action_retcode = r["retcode"] alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"] alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"] alert.action_stderr = r["stderr"]
@@ -482,24 +321,21 @@ class Alert(models.Model):
else: else:
DebugLog.error( DebugLog.error(
agent=agent, agent=agent,
log_type=DebugLogType.SCRIPTING, log_type="scripting",
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert", message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
) )
@classmethod @classmethod
def handle_alert_resolve( def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
cls, instance: Union[Agent, TaskResult, CheckResult]
) -> None:
from agents.models import Agent from agents.models import Agent
from autotasks.models import TaskResult from autotasks.models import AutomatedTask
from checks.models import CheckResult from checks.models import Check
# set variables # set variables
email_on_resolved = False email_on_resolved = False
text_on_resolved = False text_on_resolved = False
resolved_email_task = None resolved_email_task = None
resolved_text_task = None resolved_text_task = None
run_script_action = None
# check what the instance passed is # check what the instance passed is
if isinstance(instance, Agent): if isinstance(instance, Agent):
@@ -509,6 +345,7 @@ class Alert(models.Model):
resolved_text_task = agent_recovery_sms_task resolved_text_task = agent_recovery_sms_task
alert_template = instance.alert_template alert_template = instance.alert_template
alert = cls.objects.get(agent=instance, resolved=False)
maintenance_mode = instance.maintenance_mode maintenance_mode = instance.maintenance_mode
agent = instance agent = instance
@@ -517,12 +354,7 @@ class Alert(models.Model):
text_on_resolved = alert_template.agent_text_on_resolved text_on_resolved = alert_template.agent_text_on_resolved
run_script_action = alert_template.agent_script_actions run_script_action = alert_template.agent_script_actions
if agent.overdue_email_alert: elif isinstance(instance, Check):
email_on_resolved = True
if agent.overdue_text_alert:
text_on_resolved = True
elif isinstance(instance, CheckResult):
from checks.tasks import ( from checks.tasks import (
handle_resolved_check_email_alert_task, handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task, handle_resolved_check_sms_alert_task,
@@ -532,6 +364,7 @@ class Alert(models.Model):
resolved_text_task = handle_resolved_check_sms_alert_task resolved_text_task = handle_resolved_check_sms_alert_task
alert_template = instance.agent.alert_template alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_check=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent agent = instance.agent
@@ -540,7 +373,7 @@ class Alert(models.Model):
text_on_resolved = alert_template.check_text_on_resolved text_on_resolved = alert_template.check_text_on_resolved
run_script_action = alert_template.check_script_actions run_script_action = alert_template.check_script_actions
elif isinstance(instance, TaskResult): elif isinstance(instance, AutomatedTask):
from autotasks.tasks import ( from autotasks.tasks import (
handle_resolved_task_email_alert, handle_resolved_task_email_alert,
handle_resolved_task_sms_alert, handle_resolved_task_sms_alert,
@@ -550,6 +383,7 @@ class Alert(models.Model):
resolved_text_task = handle_resolved_task_sms_alert resolved_text_task = handle_resolved_task_sms_alert
alert_template = instance.agent.alert_template alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_task=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent agent = instance.agent
@@ -561,10 +395,8 @@ class Alert(models.Model):
else: else:
return return
alert = instance.get_or_create_alert_if_needed(alert_template)
# return if agent is in maintenance mode # return if agent is in maintenance mode
if not alert or maintenance_mode: if maintenance_mode:
return return
alert.resolve() alert.resolve()
@@ -581,7 +413,7 @@ class Alert(models.Model):
if ( if (
alert_template alert_template
and alert_template.resolved_action and alert_template.resolved_action
and run_script_action and run_script_action # type: ignore
and not alert.resolved_action_run and not alert.resolved_action_run
): ):
r = agent.run_script( r = agent.run_script(
@@ -594,7 +426,7 @@ class Alert(models.Model):
) )
# command was successful # command was successful
if isinstance(r, dict): if type(r) == dict:
alert.resolved_action_retcode = r["retcode"] alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"] alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"] alert.resolved_action_stderr = r["stderr"]
@@ -606,11 +438,11 @@ class Alert(models.Model):
else: else:
DebugLog.error( DebugLog.error(
agent=agent, agent=agent,
log_type=DebugLogType.SCRIPTING, log_type="scripting",
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert", message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
) )
def parse_script_args(self, args: List[str]) -> List[str]: def parse_script_args(self, args: list[str]):
if not args: if not args:
return [] return []
@@ -631,9 +463,9 @@ class Alert(models.Model):
continue continue
try: try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e: except Exception as e:
DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e)) DebugLog.error(log_type="scripting", message=str(e))
continue continue
else: else:
@@ -703,17 +535,17 @@ class AlertTemplate(BaseAuditModel):
# check alert settings # check alert settings
check_email_alert_severity = ArrayField( check_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
check_text_alert_severity = ArrayField( check_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
check_dashboard_alert_severity = ArrayField( check_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
@@ -727,17 +559,17 @@ class AlertTemplate(BaseAuditModel):
# task alert settings # task alert settings
task_email_alert_severity = ArrayField( task_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
task_text_alert_severity = ArrayField( task_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
task_dashboard_alert_severity = ArrayField( task_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True, blank=True,
default=list, default=list,
) )
@@ -763,22 +595,11 @@ class AlertTemplate(BaseAuditModel):
"agents.Agent", related_name="alert_exclusions", blank=True "agents.Agent", related_name="alert_exclusions", blank=True
) )
def __str__(self) -> str: def __str__(self):
return self.name return self.name
def is_agent_excluded(self, agent: "Agent") -> bool:
return (
agent in self.excluded_agents.all()
or agent.site in self.excluded_sites.all()
or agent.client in self.excluded_clients.all()
or agent.monitoring_type == AgentMonType.WORKSTATION
and self.exclude_workstations
or agent.monitoring_type == AgentMonType.SERVER
and self.exclude_servers
)
@staticmethod @staticmethod
def serialize(alert_template: AlertTemplate) -> Dict[str, Any]: def serialize(alert_template):
# serializes the agent and returns json # serializes the agent and returns json
from .serializers import AlertTemplateAuditSerializer from .serializers import AlertTemplateAuditSerializer

View File

@@ -1,15 +1,10 @@
from typing import TYPE_CHECKING
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import permissions from rest_framework import permissions
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
if TYPE_CHECKING:
from accounts.models import User
def _has_perm_on_alert(user, id: int):
def _has_perm_on_alert(user: "User", id: int) -> bool:
from alerts.models import Alert from alerts.models import Alert
role = user.role role = user.role
@@ -24,6 +19,10 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
if alert.agent: if alert.agent:
agent_id = alert.agent.agent_id agent_id = alert.agent.agent_id
elif alert.assigned_check:
agent_id = alert.assigned_check.agent.agent_id
elif alert.assigned_task:
agent_id = alert.assigned_task.agent.agent_id
else: else:
return True return True
@@ -31,7 +30,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
class AlertPerms(permissions.BasePermission): class AlertPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET" or r.method == "PATCH": if r.method == "GET" or r.method == "PATCH":
if "pk" in view.kwargs.keys(): if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert( return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
@@ -49,7 +48,7 @@ class AlertPerms(permissions.BasePermission):
class AlertTemplatePerms(permissions.BasePermission): class AlertTemplatePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_alerttemplates") return _has_perm(r, "can_list_alerttemplates")
else: else:

View File

@@ -3,17 +3,103 @@ from rest_framework.serializers import ModelSerializer, ReadOnlyField
from automation.serializers import PolicySerializer from automation.serializers import PolicySerializer
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
from tacticalrmm.utils import get_default_timezone
from .models import Alert, AlertTemplate from .models import Alert, AlertTemplate
class AlertSerializer(ModelSerializer): class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="assigned_agent.hostname") hostname = SerializerMethodField()
agent_id = ReadOnlyField(source="assigned_agent.agent_id") agent_id = SerializerMethodField()
client = ReadOnlyField(source="client.name") client = SerializerMethodField()
site = ReadOnlyField(source="site.name") site = SerializerMethodField()
alert_time = ReadOnlyField() alert_time = SerializerMethodField()
resolve_on = SerializerMethodField()
snoozed_until = SerializerMethodField()
def get_agent_id(self, instance):
if instance.alert_type == "availability":
return instance.agent.agent_id if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.agent_id
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.agent_id if instance.assigned_task else ""
)
else:
return ""
def get_hostname(self, instance):
if instance.alert_type == "availability":
return instance.agent.hostname if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.hostname
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.hostname if instance.assigned_task else ""
)
else:
return ""
def get_client(self, instance):
if instance.alert_type == "availability":
return instance.agent.client.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.client.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.client.name
if instance.assigned_task
else ""
)
else:
return ""
def get_site(self, instance):
if instance.alert_type == "availability":
return instance.agent.site.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.site.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.site.name if instance.assigned_task else ""
)
else:
return ""
def get_alert_time(self, instance):
if instance.alert_time:
return instance.alert_time.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_resolve_on(self, instance):
if instance.resolved_on:
return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_snoozed_until(self, instance):
if instance.snooze_until:
return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
return None
class Meta: class Meta:
model = Alert model = Alert
@@ -35,11 +121,11 @@ class AlertTemplateSerializer(ModelSerializer):
fields = "__all__" fields = "__all__"
def get_applied_count(self, instance): def get_applied_count(self, instance):
return ( count = 0
instance.policies.count() count += instance.policies.count()
+ instance.clients.count() count += instance.clients.count()
+ instance.sites.count() count += instance.sites.count()
) return count
class AlertTemplateRelationSerializer(ModelSerializer): class AlertTemplateRelationSerializer(ModelSerializer):

View File

@@ -1,13 +1,11 @@
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from agents.models import Agent
from tacticalrmm.celery import app from tacticalrmm.celery import app
from .models import Alert
@app.task @app.task
def unsnooze_alerts() -> str: def unsnooze_alerts() -> str:
from .models import Alert
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update( Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
snoozed=False, snooze_until=None snoozed=False, snooze_until=None
) )
@@ -16,10 +14,10 @@ def unsnooze_alerts() -> str:
@app.task @app.task
def cache_agents_alert_template() -> str: def cache_agents_alert_template():
for agent in Agent.objects.only( from agents.models import Agent
"pk", "site", "policy", "alert_template"
).select_related("site", "policy", "alert_template"): for agent in Agent.objects.only("pk"):
agent.set_alert_template() agent.set_alert_template()
return "ok" return "ok"
@@ -27,6 +25,8 @@ def cache_agents_alert_template() -> str:
@app.task @app.task
def prune_resolved_alerts(older_than_days: int) -> str: def prune_resolved_alerts(older_than_days: int) -> str:
from .models import Alert
Alert.objects.filter(resolved=True).filter( Alert.objects.filter(resolved=True).filter(
alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
).delete() ).delete()

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from tacticalrmm.helpers import notify_error from tacticalrmm.utils import notify_error
from .models import Alert, AlertTemplate from .models import Alert, AlertTemplate
from .permissions import AlertPerms, AlertTemplatePerms from .permissions import AlertPerms, AlertTemplatePerms
@@ -92,7 +92,7 @@ class GetAddAlerts(APIView):
) )
alerts = ( alerts = (
Alert.objects.filter_by_role(request.user) # type: ignore Alert.objects.filter_by_role(request.user)
.filter(clientFilter) .filter(clientFilter)
.filter(severityFilter) .filter(severityFilter)
.filter(resolvedFilter) .filter(resolvedFilter)
@@ -102,7 +102,7 @@ class GetAddAlerts(APIView):
return Response(AlertSerializer(alerts, many=True).data) return Response(AlertSerializer(alerts, many=True).data)
else: else:
alerts = Alert.objects.filter_by_role(request.user) # type: ignore alerts = Alert.objects.filter_by_role(request.user)
return Response(AlertSerializer(alerts, many=True).data) return Response(AlertSerializer(alerts, many=True).data)
def post(self, request): def post(self, request):

View File

@@ -1,8 +1,12 @@
import json
import os
from unittest.mock import patch
from django.conf import settings
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from model_bakery import baker from model_bakery import baker
from autotasks.models import TaskResult from autotasks.models import AutomatedTask
from tacticalrmm.constants import CustomFieldModel, CustomFieldType, TaskStatus
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
@@ -13,53 +17,46 @@ class TestAPIv3(TacticalTestCase):
self.agent = baker.make_recipe("agents.agent") self.agent = baker.make_recipe("agents.agent")
def test_get_checks(self): def test_get_checks(self):
agent = baker.make_recipe("agents.agent") url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
url = f"/api/v3/{agent.agent_id}/checkrunner/"
# add a check # add a check
check1 = baker.make_recipe("checks.ping_check", agent=agent) check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
check_result1 = baker.make(
"checks.CheckResult", agent=agent, assigned_check=check1
)
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], self.agent.check_interval) self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
self.assertEqual(len(r.data["checks"]), 1) self.assertEqual(len(r.data["checks"]), 1) # type: ignore
# override check run interval # override check run interval
check2 = baker.make_recipe( check2 = baker.make_recipe(
"checks.diskspace_check", agent=agent, run_interval=20 "checks.ping_check", agent=self.agent, run_interval=20
)
check_result2 = baker.make(
"checks.CheckResult", agent=agent, assigned_check=check2
) )
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["checks"]), 2) self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEqual(r.data["check_interval"], 20) self.assertEqual(len(r.data["checks"]), 2) # type: ignore
# Set last_run on both checks and should return an empty list # Set last_run on both checks and should return an empty list
check_result1.last_run = djangotime.now() check1.last_run = djangotime.now()
check_result1.save() check1.save()
check_result2.last_run = djangotime.now() check2.last_run = djangotime.now()
check_result2.save() check2.save()
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertFalse(r.data["checks"]) self.assertFalse(r.data["checks"]) # type: ignore
# set last_run greater than interval # set last_run greater than interval
check_result1.last_run = djangotime.now() - djangotime.timedelta(seconds=200) check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check_result1.save() check1.save()
check_result2.last_run = djangotime.now() - djangotime.timedelta(seconds=200) check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check_result2.save() check2.save()
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEqual(len(r.data["checks"]), 2) self.assertEquals(len(r.data["checks"]), 2) # type: ignore
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/" url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
r = self.client.get(url) r = self.client.get(url)
@@ -67,6 +64,24 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
def test_sysinfo(self):
# TODO replace this with golang wmi sample data
url = "/api/v3/sysinfo/"
with open(
os.path.join(
settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
)
) as f:
wmi_py = json.load(f)
payload = {"agent_id": self.agent.agent_id, "sysinfo": wmi_py}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url)
def test_checkrunner_interval(self): def test_checkrunner_interval(self):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/" url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json") r = self.client.get(url, format="json")
@@ -115,31 +130,61 @@ class TestAPIv3(TacticalTestCase):
self.assertIsInstance(r.json()["check_interval"], int) self.assertIsInstance(r.json()["check_interval"], int)
self.assertEqual(len(r.json()["checks"]), 15) self.assertEqual(len(r.json()["checks"]), 15)
def test_task_runner_get(self): @patch("apiv3.views.reload_nats")
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/") def test_agent_recovery(self, reload_nats):
reload_nats.return_value = "ok"
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
self.assertEqual(r.status_code, 404) self.assertEqual(r.status_code, 404)
script = baker.make("scripts.script") agent = baker.make_recipe("agents.online_agent")
url = f"/api/v3/{agent.agent_id}/recovery/"
# setup data
task_actions = [
{"type": "cmd", "command": "whoami", "timeout": 10, "shell": "cmd"},
{
"type": "script",
"script": script.id,
"script_args": ["test"],
"timeout": 30,
},
{"type": "script", "script": 3, "script_args": [], "timeout": 30},
]
agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent, actions=task_actions)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make(
"agents.RecoveryAction",
agent=agent,
mode="command",
command="shutdown /r /t 5 /f",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
)
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()
def test_task_runner_get(self):
from autotasks.serializers import TaskGOGetSerializer
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
# setup data
agent = baker.make_recipe("agents.agent")
script = baker.make_recipe("scripts.script")
task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore
def test_task_runner_results(self): def test_task_runner_results(self):
from agents.models import AgentCustomField from agents.models import AgentCustomField
@@ -150,9 +195,8 @@ class TestAPIv3(TacticalTestCase):
# setup data # setup data
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent) task = baker.make("autotasks.AutomatedTask", agent=agent)
task_result = baker.make("autotasks.TaskResult", agent=agent, task=task)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
# test passing task # test passing task
data = { data = {
@@ -164,9 +208,7 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertTrue( self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.PASSING
)
# test failing task # test failing task
data = { data = {
@@ -178,33 +220,20 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertTrue( self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
)
# test collector task # test collector task
text = baker.make( text = baker.make("core.CustomField", model="agent", type="text", name="Test")
"core.CustomField",
model=CustomFieldModel.AGENT,
type=CustomFieldType.TEXT,
name="Test",
)
boolean = baker.make( boolean = baker.make(
"core.CustomField", "core.CustomField", model="agent", type="checkbox", name="Test1"
model=CustomFieldModel.AGENT,
type=CustomFieldType.CHECKBOX,
name="Test1",
) )
multiple = baker.make( multiple = baker.make(
"core.CustomField", "core.CustomField", model="agent", type="multiple", name="Test2"
model=CustomFieldModel.AGENT,
type=CustomFieldType.MULTIPLE,
name="Test2",
) )
# test text fields # test text fields
task.custom_field = text task.custom_field = text # type: ignore
task.save() task.save() # type: ignore
# test failing failing with stderr # test failing failing with stderr
data = { data = {
@@ -216,9 +245,7 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertTrue( self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
)
# test saving to text field # test saving to text field
data = { data = {
@@ -230,17 +257,12 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual( self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore
)
self.assertEqual(
AgentCustomField.objects.get(field=text, agent=task.agent).value,
"the last line",
)
# test saving to checkbox field # test saving to checkbox field
task.custom_field = boolean task.custom_field = boolean # type: ignore
task.save() task.save() # type: ignore
data = { data = {
"stdout": "1", "stdout": "1",
@@ -251,16 +273,12 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual( self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore
)
self.assertTrue(
AgentCustomField.objects.get(field=boolean, agent=task.agent).value
)
# test saving to multiple field with commas # test saving to multiple field with commas
task.custom_field = multiple task.custom_field = multiple # type: ignore
task.save() task.save() # type: ignore
data = { data = {
"stdout": "this,is,an,array", "stdout": "this,is,an,array",
@@ -271,13 +289,8 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual( self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore
)
self.assertEqual(
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
["this", "is", "an", "array"],
)
# test mutiple with a single value # test mutiple with a single value
data = { data = {
@@ -289,10 +302,5 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data) r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual( self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore
)
self.assertEqual(
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
["this"],
)

View File

@@ -9,6 +9,7 @@ urlpatterns = [
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()), path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()), path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("meshexe/", views.MeshExe.as_view()), path("meshexe/", views.MeshExe.as_view()),
path("sysinfo/", views.SysInfo.as_view()),
path("newagent/", views.NewAgent.as_view()), path("newagent/", views.NewAgent.as_view()),
path("software/", views.Software.as_view()), path("software/", views.Software.as_view()),
path("installer/", views.Installer.as_view()), path("installer/", views.Installer.as_view()),
@@ -18,5 +19,6 @@ urlpatterns = [
path("winupdates/", views.WinUpdates.as_view()), path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()), path("superseded/", views.SupersededWinUpdate.as_view()),
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()), path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()), path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
] ]

View File

@@ -1,7 +1,9 @@
import asyncio import asyncio
import os
import time
from django.conf import settings from django.conf import settings
from django.db.models import Prefetch from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from packaging import version as pyver from packaging import version as pyver
@@ -14,33 +16,13 @@ from rest_framework.views import APIView
from accounts.models import User from accounts.models import User
from agents.models import Agent, AgentHistory from agents.models import Agent, AgentHistory
from agents.serializers import AgentHistorySerializer from agents.serializers import AgentHistorySerializer
from autotasks.models import AutomatedTask, TaskResult from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER from checks.models import Check
from checks.models import Check, CheckResult
from checks.serializers import CheckRunnerGetSerializer from checks.serializers import CheckRunnerGetSerializer
from core.utils import ( from logs.models import PendingAction, DebugLog
download_mesh_agent,
get_core_settings,
get_mesh_device_id,
get_mesh_ws_url,
)
from logs.models import DebugLog, PendingAction
from software.models import InstalledSoftware from software.models import InstalledSoftware
from tacticalrmm.constants import ( from tacticalrmm.utils import notify_error, reload_nats
AGENT_DEFER,
AgentMonType,
AgentPlat,
AuditActionType,
AuditObjType,
CheckStatus,
DebugLogType,
GoArch,
MeshAgentIdent,
PAStatus,
)
from tacticalrmm.helpers import notify_error
from tacticalrmm.utils import reload_nats
from winupdate.models import WinUpdate, WinUpdatePolicy from winupdate.models import WinUpdate, WinUpdatePolicy
@@ -51,12 +33,11 @@ class CheckIn(APIView):
# called once during tacticalagent windows service startup # called once during tacticalagent windows service startup
def post(self, request): def post(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
if not agent.choco_installed: if not agent.choco_installed:
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
time.sleep(0.5)
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok") return Response("ok")
@@ -66,9 +47,7 @@ class SyncMeshNodeID(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
if agent.mesh_node_id != request.data["nodeid"]: if agent.mesh_node_id != request.data["nodeid"]:
agent.mesh_node_id = request.data["nodeid"] agent.mesh_node_id = request.data["nodeid"]
agent.save(update_fields=["mesh_node_id"]) agent.save(update_fields=["mesh_node_id"])
@@ -81,9 +60,7 @@ class Choco(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
agent.choco_installed = request.data["installed"] agent.choco_installed = request.data["installed"]
agent.save(update_fields=["choco_installed"]) agent.save(update_fields=["choco_installed"])
return Response("ok") return Response("ok")
@@ -94,9 +71,7 @@ class WinUpdates(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def put(self, request): def put(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
needs_reboot: bool = request.data["needs_reboot"] needs_reboot: bool = request.data["needs_reboot"]
agent.needs_reboot = needs_reboot agent.needs_reboot = needs_reboot
@@ -114,7 +89,7 @@ class WinUpdates(APIView):
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
DebugLog.info( DebugLog.info(
agent=agent, agent=agent,
log_type=DebugLogType.WIN_UPDATES, log_type="windows_updates",
message=f"{agent.hostname} is rebooting after updates were installed.", message=f"{agent.hostname} is rebooting after updates were installed.",
) )
@@ -122,13 +97,8 @@ class WinUpdates(APIView):
return Response("ok") return Response("ok")
def patch(self, request): def patch(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
if not u:
raise WinUpdate.DoesNotExist
success: bool = request.data["success"] success: bool = request.data["success"]
if success: if success:
u.result = "success" u.result = "success"
@@ -151,14 +121,8 @@ class WinUpdates(APIView):
return Response("ok") return Response("ok")
def post(self, request): def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"] updates = request.data["wua_updates"]
if not updates:
return notify_error("Empty payload")
agent = get_object_or_404(
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
for update in updates: for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
@@ -197,9 +161,7 @@ class SupersededWinUpdate(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
for u in updates: for u in updates:
u.delete() u.delete()
@@ -212,19 +174,12 @@ class RunChecks(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, agentid): def get(self, request, agentid):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=agentid)
Agent.objects.defer(*AGENT_DEFER).prefetch_related( checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
),
agent_id=agentid,
)
checks = agent.get_checks_with_policies(exclude_overridden=True)
ret = { ret = {
"agent": agent.pk, "agent": agent.pk,
"check_interval": agent.check_interval, "check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializer( "checks": CheckRunnerGetSerializer(checks, many=True).data,
checks, context={"agent": agent}, many=True
).data,
} }
return Response(ret) return Response(ret)
@@ -234,72 +189,45 @@ class CheckRunner(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, agentid): def get(self, request, agentid):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=agentid)
Agent.objects.defer(*AGENT_DEFER).prefetch_related( checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
),
agent_id=agentid,
)
checks = agent.get_checks_with_policies(exclude_overridden=True)
run_list = [ run_list = [
check check
for check in checks for check in checks
# always run if check hasn't run yet # always run if check hasn't run yet
if not isinstance(check.check_result, CheckResult) if not check.last_run
or not check.check_result.last_run # if a check interval is set, see if the correct amount of seconds have passed
# see if the correct amount of seconds have passed
or ( or (
check.check_result.last_run check.run_interval
and (
check.last_run
< djangotime.now() < djangotime.now()
- djangotime.timedelta( - djangotime.timedelta(seconds=check.run_interval)
seconds=check.run_interval
if check.run_interval
else agent.check_interval
) )
) )
# if check interval isn't set, make sure the agent's check interval has passed before running
or (
not check.run_interval
and check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
] ]
ret = { ret = {
"agent": agent.pk, "agent": agent.pk,
"check_interval": agent.check_run_interval(), "check_interval": agent.check_run_interval(),
"checks": CheckRunnerGetSerializer( "checks": CheckRunnerGetSerializer(run_list, many=True).data,
run_list, context={"agent": agent}, many=True
).data,
} }
return Response(ret) return Response(ret)
def patch(self, request): def patch(self, request):
if "agent_id" not in request.data.keys(): check = get_object_or_404(Check, pk=request.data["id"])
return notify_error("Agent upgrade required")
check = get_object_or_404( check.last_run = djangotime.now()
Check.objects.defer(*CHECK_DEFER), check.save(update_fields=["last_run"])
pk=request.data["id"], status = check.handle_check(request.data)
) if status == "failing" and check.assignedtask.exists(): # type: ignore
agent = get_object_or_404( check.handle_assigned_task()
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
)
# get check result or create if doesn't exist
check_result, created = CheckResult.objects.defer(
*CHECK_RESULT_DEFER
).get_or_create(
assigned_check=check,
agent=agent,
)
if created:
check_result.save()
status = check_result.handle_check(request.data, check, agent)
if status == CheckStatus.FAILING and check.assignedtasks.exists():
for task in check.assignedtasks.all():
if task.enabled:
if task.policy:
task.run_win_task(agent)
else:
task.run_win_task()
return Response("ok") return Response("ok")
@@ -309,10 +237,7 @@ class CheckRunnerInterval(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, agentid): def get(self, request, agentid):
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=agentid)
Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"),
agent_id=agentid,
)
return Response( return Response(
{"agent": agent.pk, "check_interval": agent.check_run_interval()} {"agent": agent.pk, "check_interval": agent.check_run_interval()}
@@ -324,71 +249,65 @@ class TaskRunner(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, pk, agentid): def get(self, request, pk, agentid):
agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid) _ = get_object_or_404(Agent, agent_id=agentid)
task = get_object_or_404(AutomatedTask, pk=pk) task = get_object_or_404(AutomatedTask, pk=pk)
return Response(TaskGOGetSerializer(task, context={"agent": agent}).data) return Response(TaskGOGetSerializer(task).data)
def patch(self, request, pk, agentid): def patch(self, request, pk, agentid):
from alerts.models import Alert from alerts.models import Alert
agent = get_object_or_404( agent = get_object_or_404(Agent, agent_id=agentid)
Agent.objects.defer(*AGENT_DEFER), task = get_object_or_404(AutomatedTask, pk=pk)
agent_id=agentid,
)
task = get_object_or_404(
AutomatedTask.objects.select_related("custom_field"), pk=pk
)
# get task result or create if doesn't exist serializer = TaskRunnerPatchSerializer(
try: instance=task, data=request.data, partial=True
task_result = (
TaskResult.objects.select_related("agent")
.defer("agent__services", "agent__wmi_detail")
.get(task=task, agent=agent)
) )
serializer = TaskResultSerializer(
data=request.data, instance=task_result, partial=True
)
except TaskResult.DoesNotExist:
serializer = TaskResultSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
task_result = serializer.save(last_run=djangotime.now()) new_task = serializer.save(last_run=djangotime.now())
AgentHistory.objects.create( AgentHistory.objects.create(
agent=agent, agent=agent,
type=AuditActionType.TASK_RUN, type="task_run",
command=task.name, script=task.script,
script_results=request.data, script_results=request.data,
) )
# check if task is a collector and update the custom field # check if task is a collector and update the custom field
if task.custom_field: if task.custom_field:
if not task_result.stderr: if not task.stderr:
task_result.save_collector_results() task.save_collector_results()
status = CheckStatus.PASSING status = "passing"
else: else:
status = CheckStatus.FAILING status = "failing"
else: else:
status = ( status = "failing" if task.retcode != 0 else "passing"
CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
)
if task_result: new_task.status = status
task_result.status = status new_task.save()
task_result.save(update_fields=["status"])
if status == "passing":
if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
Alert.handle_alert_resolve(new_task)
else: else:
task_result.status = status Alert.handle_alert_failure(new_task)
task.save(update_fields=["status"])
if status == CheckStatus.PASSING: return Response("ok")
if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
Alert.handle_alert_resolve(task_result)
else:
Alert.handle_alert_failure(task_result)
class SysInfo(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if not isinstance(request.data["sysinfo"], dict):
return notify_error("err")
agent.wmi_detail = request.data["sysinfo"]
agent.save(update_fields=["wmi_detail"])
return Response("ok") return Response("ok")
@@ -396,33 +315,25 @@ class MeshExe(APIView):
"""Sends the mesh exe to the installer""" """Sends the mesh exe to the installer"""
def post(self, request): def post(self, request):
match request.data: exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}: mesh_exe = os.path.join(settings.EXE_DIR, exe)
arch = MeshAgentIdent.WIN64
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
arch = MeshAgentIdent.WIN32
case _:
return notify_error("Arch not specified")
core = get_core_settings() if not os.path.exists(mesh_exe):
return notify_error("Mesh Agent executable not found")
try: if settings.DEBUG:
uri = get_mesh_ws_url() with open(mesh_exe, "rb") as f:
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) response = HttpResponse(
except: f.read(),
return notify_error("Unable to connect to mesh to get group id information") content_type="application/vnd.microsoft.portable-executable",
if settings.DOCKER_BUILD:
dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
else:
dl_url = (
f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
) )
response["Content-Disposition"] = f"inline; filename={exe}"
try: return response
return download_mesh_agent(dl_url) else:
except: response = HttpResponse()
return notify_error("Unable to download mesh agent exe") response["Content-Disposition"] = f"attachment; filename={exe}"
response["X-Accel-Redirect"] = f"/private/exe/{exe}"
return response
class NewAgent(APIView): class NewAgent(APIView):
@@ -443,11 +354,11 @@ class NewAgent(APIView):
monitoring_type=request.data["monitoring_type"], monitoring_type=request.data["monitoring_type"],
description=request.data["description"], description=request.data["description"],
mesh_node_id=request.data["mesh_node_id"], mesh_node_id=request.data["mesh_node_id"],
goarch=request.data["goarch"],
plat=request.data["plat"],
last_seen=djangotime.now(), last_seen=djangotime.now(),
) )
agent.save() agent.save()
agent.salt_id = f"{agent.hostname}-{agent.pk}"
agent.save(update_fields=["salt_id"])
user = User.objects.create_user( # type: ignore user = User.objects.create_user( # type: ignore
username=request.data["agent_id"], username=request.data["agent_id"],
@@ -457,7 +368,7 @@ class NewAgent(APIView):
token = Token.objects.create(user=user) token = Token.objects.create(user=user)
if agent.monitoring_type == AgentMonType.WORKSTATION: if agent.monitoring_type == "workstation":
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
else: else:
WinUpdatePolicy(agent=agent).save() WinUpdatePolicy(agent=agent).save()
@@ -468,15 +379,20 @@ class NewAgent(APIView):
AuditLog.objects.create( AuditLog.objects.create(
username=request.user, username=request.user,
agent=agent.hostname, agent=agent.hostname,
object_type=AuditObjType.AGENT, object_type="agent",
action=AuditActionType.AGENT_INSTALL, action="agent_install",
message=f"{request.user} installed new agent {agent.hostname}", message=f"{request.user} installed new agent {agent.hostname}",
after_value=Agent.serialize(agent), after_value=Agent.serialize(agent),
debug_info={"ip": request._client_ip}, debug_info={"ip": request._client_ip},
) )
ret = {"pk": agent.pk, "token": token.key} return Response(
return Response(ret) {
"pk": agent.pk,
"saltid": f"{agent.hostname}-{agent.pk}",
"token": token.key,
}
)
class Software(APIView): class Software(APIView):
@@ -506,10 +422,7 @@ class Installer(APIView):
return notify_error("Invalid data") return notify_error("Invalid data")
ver = request.data["version"] ver = request.data["version"]
if ( if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER):
pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
and not "-dev" in settings.LATEST_AGENT_VER
):
return notify_error( return notify_error(
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM" f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
) )
@@ -544,19 +457,53 @@ class ChocoResult(APIView):
action.details["output"] = results action.details["output"] = results
action.details["installed"] = installed action.details["installed"] = installed
action.status = PAStatus.COMPLETED action.status = "completed"
action.save(update_fields=["details", "status"]) action.save(update_fields=["details", "status"])
return Response("ok") return Response("ok")
class AgentRecovery(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(
Agent.objects.prefetch_related("recoveryactions").only(
"pk", "agent_id", "last_seen"
),
agent_id=agentid,
)
# TODO remove these 2 lines after agent v1.7.0 has been out for a while
# this is handled now by nats-api service
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
ret = {"mode": "pass", "shellcmd": ""}
if recovery is None:
return Response(ret)
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
ret["mode"] = recovery.mode
if recovery.mode == "command":
ret["shellcmd"] = recovery.command
elif recovery.mode == "rpc":
reload_nats()
return Response(ret)
class AgentHistoryResult(APIView): class AgentHistoryResult(APIView):
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def patch(self, request, agentid, pk): def patch(self, request, agentid, pk):
hist = get_object_or_404( _ = get_object_or_404(Agent, agent_id=agentid)
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk hist = get_object_or_404(AgentHistory, pk=pk)
)
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True) s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
s.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
s.save() s.save()

View File

@@ -1,21 +1,8 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from django.core.cache import cache
from django.db import models from django.db import models
from agents.models import Agent from agents.models import Agent
from clients.models import Client, Site from core.models import CoreSettings
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
from tacticalrmm.constants import (
CORESETTINGS_CACHE_KEY,
AgentMonType,
AgentPlat,
CheckType,
)
if TYPE_CHECKING:
from autotasks.models import AutomatedTask
from checks.models import Check
class Policy(BaseAuditModel): class Policy(BaseAuditModel):
@@ -40,301 +27,366 @@ class Policy(BaseAuditModel):
"agents.Agent", related_name="policy_exclusions", blank=True "agents.Agent", related_name="policy_exclusions", blank=True
) )
def save(self, *args: Any, **kwargs: Any) -> None: def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_task
# get old policy if exists # get old policy if exists
old_policy: Optional[Policy] = ( old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
type(self).objects.get(pk=self.pk) if self.pk else None
)
super(Policy, self).save(old_model=old_policy, *args, **kwargs) super(Policy, self).save(old_model=old_policy, *args, **kwargs)
# check if alert template was changes and cache on agents # generate agent checks only if active and enforced were changed
if old_policy: if old_policy:
if old_policy.alert_template != self.alert_template:
cache_agents_alert_template.delay()
elif self.alert_template and old_policy.active != self.active:
cache_agents_alert_template.delay()
if old_policy.active != self.active or old_policy.enforced != self.enforced: if old_policy.active != self.active or old_policy.enforced != self.enforced:
cache.delete(CORESETTINGS_CACHE_KEY) generate_agent_checks_task.delay(
cache.delete_many_pattern("site_workstation_*") policy=self.pk,
cache.delete_many_pattern("site_server_*") create_tasks=True,
cache.delete_many_pattern("agent_*")
def delete(self, *args, **kwargs):
cache.delete(CORESETTINGS_CACHE_KEY)
cache.delete_many_pattern("site_workstation_*")
cache.delete_many_pattern("site_server_*")
cache.delete_many_pattern("agent_*")
super(Policy, self).delete(
*args,
**kwargs,
) )
def __str__(self) -> str: if old_policy.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
super(Policy, self).delete(*args, **kwargs)
generate_agent_checks_task.delay(agents=agents, create_tasks=True)
def __str__(self):
return self.name return self.name
@property @property
def is_default_server_policy(self) -> bool: def is_default_server_policy(self):
return self.default_server_policy.exists() return self.default_server_policy.exists() # type: ignore
@property @property
def is_default_workstation_policy(self) -> bool: def is_default_workstation_policy(self):
return self.default_workstation_policy.exists() return self.default_workstation_policy.exists() # type: ignore
def is_agent_excluded(self, agent: "Agent") -> bool: def is_agent_excluded(self, agent):
return ( return (
agent in self.excluded_agents.all() agent in self.excluded_agents.all()
or agent.site in self.excluded_sites.all() or agent.site in self.excluded_sites.all()
or agent.client in self.excluded_clients.all() or agent.client in self.excluded_clients.all()
) )
def related_agents( def related_agents(self):
self, mon_type: Optional[str] = None return self.get_related("server") | self.get_related("workstation")
) -> "models.QuerySet[Agent]":
models.prefetch_related_objects(
[self],
"excluded_agents",
"excluded_sites",
"excluded_clients",
"workstation_clients",
"server_clients",
"workstation_sites",
"server_sites",
"agents",
)
agent_filter = {}
filtered_agents_ids = Agent.objects.none()
if mon_type:
agent_filter["monitoring_type"] = mon_type
excluded_clients_ids = self.excluded_clients.only("pk").values_list(
"id", flat=True
)
excluded_sites_ids = self.excluded_sites.only("pk").values_list("id", flat=True)
excluded_agents_ids = self.excluded_agents.only("pk").values_list(
"id", flat=True
)
if self.is_default_server_policy:
filtered_agents_ids |= (
Agent.objects.exclude(block_policy_inheritance=True)
.exclude(site__block_policy_inheritance=True)
.exclude(site__client__block_policy_inheritance=True)
.exclude(id__in=excluded_agents_ids)
.exclude(site_id__in=excluded_sites_ids)
.exclude(site__client_id__in=excluded_clients_ids)
.filter(monitoring_type=AgentMonType.SERVER)
.only("id")
.values_list("id", flat=True)
)
if self.is_default_workstation_policy:
filtered_agents_ids |= (
Agent.objects.exclude(block_policy_inheritance=True)
.exclude(site__block_policy_inheritance=True)
.exclude(site__client__block_policy_inheritance=True)
.exclude(id__in=excluded_agents_ids)
.exclude(site_id__in=excluded_sites_ids)
.exclude(site__client_id__in=excluded_clients_ids)
.filter(monitoring_type=AgentMonType.WORKSTATION)
.only("id")
.values_list("id", flat=True)
)
# if this is the default policy for servers and workstations and skip the other calculations
if self.is_default_server_policy and self.is_default_workstation_policy:
return Agent.objects.filter(models.Q(id__in=filtered_agents_ids))
def get_related(self, mon_type):
explicit_agents = ( explicit_agents = (
self.agents.filter(**agent_filter) # type: ignore self.agents.filter(monitoring_type=mon_type) # type: ignore
.exclude(id__in=excluded_agents_ids) .exclude(
.exclude(site_id__in=excluded_sites_ids) pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
.exclude(site__client_id__in=excluded_clients_ids) )
.exclude(site__in=self.excluded_sites.all())
.exclude(site__client__in=self.excluded_clients.all())
) )
explicit_clients_qs = Client.objects.none() explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
explicit_sites_qs = Site.objects.none() pk__in=self.excluded_clients.all()
if not mon_type or mon_type == AgentMonType.WORKSTATION:
explicit_clients_qs |= self.workstation_clients.exclude( # type: ignore
id__in=excluded_clients_ids
) )
explicit_sites_qs |= self.workstation_sites.exclude( # type: ignore explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
id__in=excluded_sites_ids pk__in=self.excluded_sites.all()
) )
if not mon_type or mon_type == AgentMonType.SERVER: filtered_agents_pks = Policy.objects.none()
explicit_clients_qs |= self.server_clients.exclude( # type: ignore
id__in=excluded_clients_ids
)
explicit_sites_qs |= self.server_sites.exclude( # type: ignore
id__in=excluded_sites_ids
)
filtered_agents_ids |= ( filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True) Agent.objects.exclude(block_policy_inheritance=True)
.filter( .filter(
site_id__in=[ site__in=[
site.id site
for site in explicit_sites_qs for site in explicit_sites
if site.client not in explicit_clients_qs if site.client not in explicit_clients
and site.client.id not in excluded_clients_ids and site.client not in self.excluded_clients.all()
], ],
**agent_filter, monitoring_type=mon_type,
) )
.only("id") .values_list("pk", flat=True)
.values_list("id", flat=True)
) )
filtered_agents_ids |= ( filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True) Agent.objects.exclude(block_policy_inheritance=True)
.exclude(site__block_policy_inheritance=True) .exclude(site__block_policy_inheritance=True)
.filter( .filter(
site__client__in=explicit_clients_qs, site__client__in=[client for client in explicit_clients],
**agent_filter, monitoring_type=mon_type,
) )
.only("id") .values_list("pk", flat=True)
.values_list("id", flat=True)
) )
return Agent.objects.filter( return Agent.objects.filter(
models.Q(id__in=filtered_agents_ids) models.Q(pk__in=filtered_agents_pks)
| models.Q(id__in=explicit_agents.only("id")) | models.Q(pk__in=explicit_agents.only("pk"))
) )
@staticmethod @staticmethod
def serialize(policy: "Policy") -> Dict[str, Any]: def serialize(policy):
# serializes the policy and returns json # serializes the policy and returns json
from .serializers import PolicyAuditSerializer from .serializers import PolicyAuditSerializer
return PolicyAuditSerializer(policy).data return PolicyAuditSerializer(policy).data
@staticmethod @staticmethod
def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]": def cascade_policy_tasks(agent):
# List of all tasks to be applied # List of all tasks to be applied
tasks = list() tasks = list()
added_task_pks = list()
agent_tasks_parent_pks = [
task.parent_task for task in agent.autotasks.filter(managed_by_policy=True)
]
# Get policies applied to agent and agent site and client # Get policies applied to agent and agent site and client
policies = agent.get_agent_policies() client = agent.client
site = agent.site
processed_policies = list() default_policy = None
client_policy = None
site_policy = None
agent_policy = agent.policy
for _, policy in policies.items(): # Get the Client/Site policy based on if the agent is server or workstation
if policy and policy.active and policy.pk not in processed_policies: if agent.monitoring_type == "server":
processed_policies.append(policy.pk) default_policy = CoreSettings.objects.first().server_policy
for task in policy.autotasks.all(): client_policy = client.server_policy
site_policy = site.server_policy
elif agent.monitoring_type == "workstation":
default_policy = CoreSettings.objects.first().workstation_policy
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
for task in agent_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task) tasks.append(task)
added_task_pks.append(task.pk)
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
for task in site_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
for task in client_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
return tasks if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
for task in default_policy.autotasks.all():
if task.pk not in added_task_pks:
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
]
):
if task.sync_status == "initial":
task.delete()
else:
task.sync_status = "pendingdeletion"
task.save()
# change tasks from pendingdeletion to notsynced if policy was added or changed
agent.autotasks.filter(sync_status="pendingdeletion").filter(
parent_task__in=[taskpk for taskpk in added_task_pks]
).update(sync_status="notsynced")
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@staticmethod @staticmethod
def get_policy_checks(agent: "Agent") -> "List[Check]": def cascade_policy_checks(agent):
# Get checks added to agent directly # Get checks added to agent directly
agent_checks = list(agent.agentchecks.all()) agent_checks = list(agent.agentchecks.filter(managed_by_policy=False))
agent_checks_parent_pks = [
check.parent_check
for check in agent.agentchecks.filter(managed_by_policy=True)
]
# Get policies applied to agent and agent site and client # Get policies applied to agent and agent site and client
policies = agent.get_agent_policies() client = agent.client
site = agent.site
default_policy = None
client_policy = None
site_policy = None
agent_policy = agent.policy
if agent.monitoring_type == "server":
default_policy = CoreSettings.objects.first().server_policy
client_policy = client.server_policy
site_policy = site.server_policy
elif agent.monitoring_type == "workstation":
default_policy = CoreSettings.objects.first().workstation_policy
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
# Used to hold the policies that will be applied and the order in which they are applied # Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first # Enforced policies are applied first
enforced_checks = list() enforced_checks = list()
policy_checks = list() policy_checks = list()
processed_policies = list() if (
agent_policy
for _, policy in policies.items(): and agent_policy.active
if policy and policy.active and policy.pk not in processed_policies: and not agent_policy.is_agent_excluded(agent)
processed_policies.append(policy.pk) ):
if policy.enforced: if agent_policy.enforced:
for check in policy.policychecks.all(): for check in agent_policy.policychecks.all():
enforced_checks.append(check) enforced_checks.append(check)
else: else:
for check in policy.policychecks.all(): for check in agent_policy.policychecks.all():
policy_checks.append(check) policy_checks.append(check)
if not enforced_checks and not policy_checks: if (
return [] site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
if site_policy.enforced:
for check in site_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in site_policy.policychecks.all():
policy_checks.append(check)
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
if client_policy.enforced:
for check in client_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in client_policy.policychecks.all():
policy_checks.append(check)
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
if default_policy.enforced:
for check in default_policy.policychecks.all():
enforced_checks.append(check)
else:
for check in default_policy.policychecks.all():
policy_checks.append(check)
# Sorted Checks already added # Sorted Checks already added
added_diskspace_checks: List[str] = list() added_diskspace_checks = list()
added_ping_checks: List[str] = list() added_ping_checks = list()
added_winsvc_checks: List[str] = list() added_winsvc_checks = list()
added_script_checks: List[int] = list() added_script_checks = list()
added_eventlog_checks: List[List[str]] = list() added_eventlog_checks = list()
added_cpuload_checks: List[int] = list() added_cpuload_checks = list()
added_memory_checks: List[int] = list() added_memory_checks = list()
# Lists all agent and policy checks that will be returned # Lists all agent and policy checks that will be created
diskspace_checks: "List[Check]" = list() diskspace_checks = list()
ping_checks: "List[Check]" = list() ping_checks = list()
winsvc_checks: "List[Check]" = list() winsvc_checks = list()
script_checks: "List[Check]" = list() script_checks = list()
eventlog_checks: "List[Check]" = list() eventlog_checks = list()
cpuload_checks: "List[Check]" = list() cpuload_checks = list()
memory_checks: "List[Check]" = list() memory_checks = list()
overridden_checks: List[int] = list()
# Loop over checks in with enforced policies first, then non-enforced policies # Loop over checks in with enforced policies first, then non-enforced policies
for check in enforced_checks + agent_checks + policy_checks: for check in enforced_checks + agent_checks + policy_checks:
if ( if check.check_type == "diskspace":
check.check_type == CheckType.DISK_SPACE
and agent.plat == AgentPlat.WINDOWS
):
# Check if drive letter was already added # Check if drive letter was already added
if check.disk not in added_diskspace_checks: if check.disk not in added_diskspace_checks:
added_diskspace_checks.append(check.disk) added_diskspace_checks.append(check.disk)
# Dont add if check if it is an agent check # Dont create the check if it is an agent check
if not check.agent: if not check.agent:
diskspace_checks.append(check) diskspace_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif check.check_type == CheckType.PING: if check.check_type == "ping":
# Check if IP/host was already added # Check if IP/host was already added
if check.ip not in added_ping_checks: if check.ip not in added_ping_checks:
added_ping_checks.append(check.ip) added_ping_checks.append(check.ip)
# Dont add if the check if it is an agent check # Dont create the check if it is an agent check
if not check.agent: if not check.agent:
ping_checks.append(check) ping_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif ( if check.check_type == "cpuload":
check.check_type == CheckType.CPU_LOAD
and agent.plat == AgentPlat.WINDOWS
):
# Check if cpuload list is empty # Check if cpuload list is empty
if not added_cpuload_checks: if not added_cpuload_checks:
added_cpuload_checks.append(check.pk) added_cpuload_checks.append(check)
# Dont create the check if it is an agent check # Dont create the check if it is an agent check
if not check.agent: if not check.agent:
cpuload_checks.append(check) cpuload_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif ( if check.check_type == "memory":
check.check_type == CheckType.MEMORY and agent.plat == AgentPlat.WINDOWS
):
# Check if memory check list is empty # Check if memory check list is empty
if not added_memory_checks: if not added_memory_checks:
added_memory_checks.append(check.pk) added_memory_checks.append(check)
# Dont create the check if it is an agent check # Dont create the check if it is an agent check
if not check.agent: if not check.agent:
memory_checks.append(check) memory_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif ( if check.check_type == "winsvc":
check.check_type == CheckType.WINSVC and agent.plat == AgentPlat.WINDOWS
):
# Check if service name was already added # Check if service name was already added
if check.svc_name not in added_winsvc_checks: if check.svc_name not in added_winsvc_checks:
added_winsvc_checks.append(check.svc_name) added_winsvc_checks.append(check.svc_name)
@@ -342,11 +394,10 @@ class Policy(BaseAuditModel):
if not check.agent: if not check.agent:
winsvc_checks.append(check) winsvc_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif check.check_type == CheckType.SCRIPT and agent.is_supported_script( if check.check_type == "script":
check.script.supported_platforms
):
# Check if script id was already added # Check if script id was already added
if check.script.id not in added_script_checks: if check.script.id not in added_script_checks:
added_script_checks.append(check.script.id) added_script_checks.append(check.script.id)
@@ -354,28 +405,20 @@ class Policy(BaseAuditModel):
if not check.agent: if not check.agent:
script_checks.append(check) script_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
elif ( if check.check_type == "eventlog":
check.check_type == CheckType.EVENT_LOG
and agent.plat == AgentPlat.WINDOWS
):
# Check if events were already added # Check if events were already added
if [check.log_name, check.event_id] not in added_eventlog_checks: if [check.log_name, check.event_id] not in added_eventlog_checks:
added_eventlog_checks.append([check.log_name, check.event_id]) added_eventlog_checks.append([check.log_name, check.event_id])
if not check.agent: if not check.agent:
eventlog_checks.append(check) eventlog_checks.append(check)
elif check.agent: elif check.agent:
overridden_checks.append(check.pk) check.overriden_by_policy = True
check.save()
if overridden_checks: final_list = (
from checks.models import Check
Check.objects.filter(pk__in=overridden_checks).update(
overridden_by_policy=True
)
return (
diskspace_checks diskspace_checks
+ ping_checks + ping_checks
+ cpuload_checks + cpuload_checks
@@ -384,3 +427,33 @@ class Policy(BaseAuditModel):
+ script_checks + script_checks
+ eventlog_checks + eventlog_checks
) )
# remove policy checks from agent that fell out of policy scope
agent.agentchecks.filter(
managed_by_policy=True,
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
]
@staticmethod
def generate_policy_checks(agent):
checks = Policy.cascade_policy_checks(agent)
if checks:
for check in checks:
check.create_policy_check(agent)
@staticmethod
def generate_policy_tasks(agent):
tasks = Policy.cascade_policy_tasks(agent)
if tasks:
for task in tasks:
task.create_policy_task(agent)

View File

@@ -4,7 +4,7 @@ from tacticalrmm.permissions import _has_perm
class AutomationPolicyPerms(permissions.BasePermission): class AutomationPolicyPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_automation_policies") return _has_perm(r, "can_list_automation_policies")
else: else:

View File

@@ -5,8 +5,8 @@ from rest_framework.serializers import (
) )
from agents.serializers import AgentHostnameSerializer from agents.serializers import AgentHostnameSerializer
from autotasks.models import TaskResult from autotasks.models import AutomatedTask
from checks.models import CheckResult from checks.models import Check
from clients.models import Client from clients.models import Client
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
@@ -96,7 +96,7 @@ class PolicyCheckStatusSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname") hostname = ReadOnlyField(source="agent.hostname")
class Meta: class Meta:
model = CheckResult model = Check
fields = "__all__" fields = "__all__"
@@ -104,7 +104,7 @@ class PolicyTaskStatusSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname") hostname = ReadOnlyField(source="agent.hostname")
class Meta: class Meta:
model = TaskResult model = AutomatedTask
fields = "__all__" fields = "__all__"

View File

@@ -1,20 +1,155 @@
from typing import Any, Dict, List, Union
from tacticalrmm.celery import app from tacticalrmm.celery import app
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
def generate_agent_checks_task(
policy: int = None,
site: int = None,
client: int = None,
agents: List[int] = list(),
all: bool = False,
create_tasks: bool = False,
) -> Union[str, None]:
from agents.models import Agent
from automation.models import Policy
p = Policy.objects.get(pk=policy) if policy else None
# generate checks on all agents if all is specified or if policy is default server/workstation policy
if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
# generate checks on all servers if policy is a default servers policy
elif p and p.is_default_server_policy:
a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
# generate checks on all workstations if policy is a default workstations policy
elif p and p.is_default_workstation_policy:
a = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
# generate checks on a list of supplied agents
elif agents:
a = Agent.objects.filter(pk__in=agents)
# generate checks on agents affected by supplied policy
elif policy:
a = p.related_agents().only("pk")
# generate checks that has specified site
elif site:
a = Agent.objects.filter(site_id=site)
# generate checks that has specified client
elif client:
a = Agent.objects.filter(site__client_id=client)
else:
a = []
for agent in a:
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
agent.set_alert_template()
return "ok"
@app.task(
acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
)
# updates policy managed check fields on agents
def update_policy_check_fields_task(check: int) -> str:
from checks.models import Check
c: Check = Check.objects.get(pk=check)
update_fields: Dict[Any, Any] = {}
for field in c.policy_fields_to_copy:
update_fields[field] = getattr(c, field)
Check.objects.filter(parent_check=check).update(**update_fields)
return "ok"
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
# generates policy tasks on agents affected by a policy
def generate_agent_autotasks_task(policy: int = None) -> str:
from agents.models import Agent
from automation.models import Policy
p: Policy = Policy.objects.get(pk=policy)
if p and p.is_default_server_policy and p.is_default_workstation_policy:
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif p and p.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif p and p.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else:
agents = p.related_agents().only("pk")
for agent in agents:
agent.generate_tasks_from_policies()
return "ok"
@app.task(
acks_late=True,
retry_backoff=5,
retry_jitter=True,
retry_kwargs={"max_retries": 5},
)
def delete_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
for t in AutomatedTask.objects.filter(parent_task=task):
t.delete_task_on_agent()
return "ok"
@app.task @app.task
def run_win_policy_autotasks_task(task: int) -> str: def run_win_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
try: for t in AutomatedTask.objects.filter(parent_task=task):
policy_task = AutomatedTask.objects.get(pk=task) t.run_win_task()
except AutomatedTask.DoesNotExist:
return "AutomatedTask not found" return "ok"
if not policy_task.policy:
return "AutomatedTask must be a policy" @app.task(
acks_late=True,
# get related agents from policy retry_backoff=5,
for agent in policy_task.policy.related_agents(): retry_jitter=True,
policy_task.run_win_task(agent) retry_kwargs={"max_retries": 5},
)
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
from autotasks.models import AutomatedTask
t = AutomatedTask.objects.get(pk=task)
update_fields: Dict[str, Any] = {}
for field in t.policy_fields_to_copy:
update_fields[field] = getattr(t, field)
AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
if update_agent:
for t in AutomatedTask.objects.filter(parent_task=task).exclude(
sync_status="initial"
):
t.modify_task_on_agent()
return "ok" return "ok"

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
from django.urls import path from django.urls import path
from autotasks.views import GetAddAutoTasks
from checks.views import GetAddChecks
from . import views from . import views
from checks.views import GetAddChecks
from autotasks.views import GetAddAutoTasks
urlpatterns = [ urlpatterns = [
path("policies/", views.GetAddPolicies.as_view()), path("policies/", views.GetAddPolicies.as_view()),
path("policies/<int:pk>/related/", views.GetRelated.as_view()), path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()), path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()), path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("sync/", views.PolicySync.as_view()),
# alias to get policy checks # alias to get policy checks
path("policies/<int:policy>/checks/", GetAddChecks.as_view()), path("policies/<int:policy>/checks/", GetAddChecks.as_view()),
# alias to get policy tasks # alias to get policy tasks

View File

@@ -1,13 +1,13 @@
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from agents.models import Agent from tacticalrmm.utils import notify_error
from autotasks.models import TaskResult
from checks.models import CheckResult
from clients.models import Client
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
@@ -16,8 +16,8 @@ from .models import Policy
from .permissions import AutomationPolicyPerms from .permissions import AutomationPolicyPerms
from .serializers import ( from .serializers import (
PolicyCheckStatusSerializer, PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicyRelatedSerializer, PolicyRelatedSerializer,
PolicyOverviewSerializer,
PolicySerializer, PolicySerializer,
PolicyTableSerializer, PolicyTableSerializer,
PolicyTaskStatusSerializer, PolicyTaskStatusSerializer,
@@ -28,9 +28,7 @@ class GetAddPolicies(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms] permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request): def get(self, request):
policies = Policy.objects.select_related("alert_template").prefetch_related( policies = Policy.objects.all()
"excluded_agents", "excluded_sites", "excluded_clients"
)
return Response( return Response(
PolicyTableSerializer( PolicyTableSerializer(
@@ -52,8 +50,8 @@ class GetAddPolicies(APIView):
check.create_policy_check(policy=policy) check.create_policy_check(policy=policy)
tasks = copyPolicy.autotasks.all() tasks = copyPolicy.autotasks.all()
for task in tasks: for task in tasks:
if not task.assigned_check:
task.create_policy_task(policy=policy) task.create_policy_task(policy=policy)
return Response("ok") return Response("ok")
@@ -68,12 +66,22 @@ class GetUpdateDeletePolicy(APIView):
return Response(PolicySerializer(policy).data) return Response(PolicySerializer(policy).data)
def put(self, request, pk): def put(self, request, pk):
from .tasks import generate_agent_checks_task
policy = get_object_or_404(Policy, pk=pk) policy = get_object_or_404(Policy, pk=pk)
serializer = PolicySerializer(instance=policy, data=request.data, partial=True) serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
# check for excluding objects and in the request and if present generate policies
if (
"excluded_sites" in request.data.keys()
or "excluded_clients" in request.data.keys()
or "excluded_agents" in request.data.keys()
):
generate_agent_checks_task.delay(policy=pk, create_tasks=True)
return Response("ok") return Response("ok")
def delete(self, request, pk): def delete(self, request, pk):
@@ -82,11 +90,25 @@ class GetUpdateDeletePolicy(APIView):
return Response("ok") return Response("ok")
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_task
generate_agent_checks_task.delay(
policy=request.data["policy"], create_tasks=True
)
return Response("ok")
else:
return notify_error("The request was invalid")
class PolicyAutoTask(APIView): class PolicyAutoTask(APIView):
# get status of all tasks # get status of all tasks
def get(self, request, task): def get(self, request, task):
tasks = TaskResult.objects.filter(task=task) tasks = AutomatedTask.objects.filter(parent_task=task)
return Response(PolicyTaskStatusSerializer(tasks, many=True).data) return Response(PolicyTaskStatusSerializer(tasks, many=True).data)
# bulk run win tasks associated with policy # bulk run win tasks associated with policy
@@ -101,16 +123,14 @@ class PolicyCheck(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms] permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request, check): def get(self, request, check):
checks = CheckResult.objects.filter(assigned_check=check) checks = Check.objects.filter(parent_check=check)
return Response(PolicyCheckStatusSerializer(checks, many=True).data) return Response(PolicyCheckStatusSerializer(checks, many=True).data)
class OverviewPolicy(APIView): class OverviewPolicy(APIView):
def get(self, request): def get(self, request):
clients = Client.objects.filter_by_role(request.user).select_related( clients = Client.objects.all()
"workstation_policy", "server_policy"
)
return Response(PolicyOverviewSerializer(clients, many=True).data) return Response(PolicyOverviewSerializer(clients, many=True).data)
@@ -141,7 +161,7 @@ class UpdatePatchPolicy(APIView):
serializer = WinUpdatePolicySerializer(data=request.data, partial=True) serializer = WinUpdatePolicySerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.policy = policy serializer.policy = policy # type: ignore
serializer.save() serializer.save()
return Response("ok") return Response("ok")
@@ -174,7 +194,7 @@ class ResetPatchPolicy(APIView):
raise PermissionDenied() raise PermissionDenied()
agents = ( agents = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy") .prefetch_related("winupdatepolicy")
.filter(site__client_id=request.data["client"]) .filter(site__client_id=request.data["client"])
) )
@@ -183,13 +203,13 @@ class ResetPatchPolicy(APIView):
raise PermissionDenied() raise PermissionDenied()
agents = ( agents = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy") .prefetch_related("winupdatepolicy")
.filter(site_id=request.data["site"]) .filter(site_id=request.data["site"])
) )
else: else:
agents = ( agents = (
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy") .prefetch_related("winupdatepolicy")
.only("pk") .only("pk")
) )

View File

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

View File

@@ -1,5 +0,0 @@
from model_bakery.recipe import Recipe
task = Recipe(
"autotasks.AutomatedTask",
)

View File

@@ -1,5 +1,6 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks from autotasks.tasks import remove_orphaned_win_tasks
@@ -7,7 +8,10 @@ class Command(BaseCommand):
help = "Checks for orphaned tasks on all agents and removes them" help = "Checks for orphaned tasks on all agents and removes them"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
remove_orphaned_win_tasks.s() agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time")
online = [i for i in agents if i.status == "online"]
for agent in online:
remove_orphaned_win_tasks.delay(agent.pk)
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(

View File

@@ -1,99 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-01 22:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0025_auto_20210917_1954"),
("agents", "0046_alter_agenthistory_command"),
("autotasks", "0029_alter_automatedtask_task_type"),
]
operations = [
migrations.RemoveField(
model_name="automatedtask",
name="retvalue",
),
migrations.AlterField(
model_name="automatedtask",
name="assigned_check",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="assignedtasks",
to="checks.check",
),
),
migrations.AlterField(
model_name="automatedtask",
name="win_task_name",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.CreateModel(
name="TaskResult",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("retcode", models.IntegerField(blank=True, null=True)),
("stdout", models.TextField(blank=True, null=True)),
("stderr", models.TextField(blank=True, null=True)),
("execution_time", models.CharField(default="0.0000", max_length=100)),
("last_run", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("passing", "Passing"),
("failing", "Failing"),
("pending", "Pending"),
],
default="pending",
max_length=30,
),
),
(
"sync_status",
models.CharField(
choices=[
("synced", "Synced With Agent"),
("notsynced", "Waiting On Agent Checkin"),
("pendingdeletion", "Pending Deletion on Agent"),
("initial", "Initial Task Sync"),
],
default="initial",
max_length=100,
),
),
(
"agent",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="taskresults",
to="agents.agent",
),
),
(
"task",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="taskresults",
to="autotasks.automatedtask",
),
),
],
options={
"unique_together": {("agent", "task")},
},
),
]

View File

@@ -1,50 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-01 22:49
from django.db import migrations, transaction
from django.db.utils import IntegrityError
def migrate_task_results(apps, schema_editor):
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
TaskResult = apps.get_model("autotasks", "TaskResult")
for task in AutomatedTask.objects.exclude(agent=None):
try:
with transaction.atomic():
if task.managed_by_policy:
TaskResult.objects.create(
task_id=task.parent_task,
agent_id=task.agent_id,
retcode=task.retcode,
stdout=task.stdout,
stderr=task.stderr,
execution_time=task.execution_time,
last_run=task.last_run,
status=task.status,
sync_status=task.sync_status,
)
else:
TaskResult.objects.create(
task_id=task.id,
agent_id=task.agent.id,
retcode=task.retcode,
stdout=task.stdout,
stderr=task.stderr,
execution_time=task.execution_time,
last_run=task.last_run,
status=task.status,
sync_status=task.sync_status,
)
except IntegrityError:
continue
class Migration(migrations.Migration):
atomic = False
dependencies = [
("autotasks", "0030_auto_20220401_2244"),
]
operations = [
migrations.RunPython(migrate_task_results),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-01 23:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0031_auto_20220401_2249'),
]
operations = [
migrations.RemoveField(
model_name='automatedtask',
name='execution_time',
),
migrations.RemoveField(
model_name='automatedtask',
name='last_run',
),
migrations.RemoveField(
model_name='automatedtask',
name='parent_task',
),
migrations.RemoveField(
model_name='automatedtask',
name='retcode',
),
migrations.RemoveField(
model_name='automatedtask',
name='status',
),
migrations.RemoveField(
model_name='automatedtask',
name='stderr',
),
migrations.RemoveField(
model_name='automatedtask',
name='stdout',
),
migrations.RemoveField(
model_name='automatedtask',
name='sync_status',
),
]

View File

@@ -1,53 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-02 00:41
from django.db import migrations
from django.utils.timezone import make_aware
from tacticalrmm.constants import TaskType
def migrate_script_data(apps, schema_editor):
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
# convert autotask to the new format
for task in AutomatedTask.objects.all():
try:
edited = False
# convert scheduled task_type
if task.task_type == TaskType.SCHEDULED:
task.task_type = TaskType.DAILY
task.run_time_date = make_aware(task.run_time_minute.strptime("%H:%M"))
task.daily_interval = 1
edited = True
# convert actions
if not task.actions:
if not task.script:
task.delete()
task.actions = [
{
"type": "script",
"script": task.script.pk,
"script_args": task.script_args,
"timeout": task.timeout,
"name": task.script.name,
}
]
edited = True
if edited:
task.save()
except:
continue
class Migration(migrations.Migration):
dependencies = [
("autotasks", "0032_auto_20220401_2301"),
]
operations = [
migrations.RunPython(migrate_script_data),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-02 00:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0033_auto_20220402_0041'),
]
operations = [
migrations.RemoveField(
model_name='automatedtask',
name='script',
),
migrations.RemoveField(
model_name='automatedtask',
name='script_args',
),
migrations.RemoveField(
model_name='automatedtask',
name='timeout',
),
]

View File

@@ -1,39 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-15 18:18
from django.db import migrations
from django.db.models import Count
from autotasks.models import generate_task_name
from tacticalrmm.constants import TaskSyncStatus
def check_for_win_task_name_duplicates(apps, schema_editor):
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
TaskResult = apps.get_model("autotasks", "TaskResult")
duplicate_tasks = (
AutomatedTask.objects.values("win_task_name")
.annotate(records=Count("win_task_name"))
.filter(records__gt=1)
)
for task in duplicate_tasks:
dups = list(AutomatedTask.objects.filter(win_task_name=task["win_task_name"]))
for x in range(task["records"] - 1):
dups[x].win_task_name = generate_task_name()
dups[x].save(update_fields=["win_task_name"])
# update task_result sync status
TaskResult.objects.filter(task=dups[x]).update(
sync_status=TaskSyncStatus.NOT_SYNCED
)
class Migration(migrations.Migration):
dependencies = [
("autotasks", "0034_auto_20220402_0046"),
]
operations = [
migrations.RunPython(check_for_win_task_name_duplicates),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 4.0.3 on 2022-04-15 20:52
from django.db import migrations, models
import autotasks.models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0035_auto_20220415_1818'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='win_task_name',
field=models.CharField(blank=True, default=autotasks.models.generate_task_name, max_length=255, unique=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.5 on 2022-06-29 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0036_alter_automatedtask_win_task_name'),
]
operations = [
migrations.AlterField(
model_name='taskresult',
name='retcode',
field=models.BigIntegerField(blank=True, null=True),
),
]

View File

@@ -1,36 +1,21 @@
import asyncio import asyncio
import datetime as dt
import random import random
import string import string
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from typing import List
from django.db.models.fields.json import JSONField
import pytz import pytz
from django.core.cache import cache from alerts.models import SEVERITY_CHOICES
from django.core.validators import MaxValueValidator, MinValueValidator from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.db.models.fields import DateTimeField from django.db.models.fields import DateTimeField
from django.db.models.fields.json import JSONField
from django.db.utils import DatabaseError from django.db.utils import DatabaseError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from core.utils import get_core_settings
from logs.models import BaseAuditModel, DebugLog from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.constants import (
FIELDS_TRIGGER_TASK_UPDATE_AGENT,
POLICY_TASK_FIELDS_TO_COPY,
AlertSeverity,
DebugLogType,
TaskStatus,
TaskSyncStatus,
TaskType,
)
if TYPE_CHECKING:
from automation.models import Policy
from alerts.models import Alert, AlertTemplate
from agents.models import Agent
from checks.models import Check
from tacticalrmm.models import PermissionQuerySet from tacticalrmm.models import PermissionQuerySet
from packaging import version as pyver
from tacticalrmm.utils import ( from tacticalrmm.utils import (
bitdays_to_string, bitdays_to_string,
bitmonthdays_to_string, bitmonthdays_to_string,
@@ -39,10 +24,29 @@ from tacticalrmm.utils import (
convert_to_iso_duration, convert_to_iso_duration,
) )
TASK_TYPE_CHOICES = [
("daily", "Daily"),
("weekly", "Weekly"),
("monthly", "Monthly"),
("monthlydow", "Monthly Day of Week"),
("checkfailure", "On Check Failure"),
("manual", "Manual"),
("runonce", "Run Once"),
("scheduled", "Scheduled"), # deprecated
]
def generate_task_name() -> str: SYNC_STATUS_CHOICES = [
chars = string.ascii_letters ("synced", "Synced With Agent"),
return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35)) ("notsynced", "Waiting On Agent Checkin"),
("pendingdeletion", "Pending Deletion on Agent"),
("initial", "Initial Task Sync"),
]
TASK_STATUS_CHOICES = [
("passing", "Passing"),
("failing", "Failing"),
("pending", "Pending"),
]
class AutomatedTask(BaseAuditModel): class AutomatedTask(BaseAuditModel):
@@ -70,21 +74,53 @@ class AutomatedTask(BaseAuditModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
# format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}] # deprecated
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="autoscript",
on_delete=models.SET_NULL,
)
# deprecated
script_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
# deprecated
timeout = models.PositiveIntegerField(blank=True, default=120)
# format -> {"actions": [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]}
actions = JSONField(default=list) actions = JSONField(default=list)
assigned_check = models.ForeignKey( assigned_check = models.ForeignKey(
"checks.Check", "checks.Check",
null=True, null=True,
blank=True, blank=True,
related_name="assignedtasks", related_name="assignedtask",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
collector_all_output = models.BooleanField(default=False) collector_all_output = models.BooleanField(default=False)
managed_by_policy = models.BooleanField(default=False)
parent_task = models.PositiveIntegerField(null=True, blank=True)
retvalue = models.TextField(null=True, blank=True)
retcode = models.IntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
execution_time = models.CharField(max_length=100, default="0.0000")
last_run = models.DateTimeField(null=True, blank=True)
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
continue_on_error = models.BooleanField(default=True) continue_on_error = models.BooleanField(default=True)
status = models.CharField(
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
)
sync_status = models.CharField(
max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
)
alert_severity = models.CharField( alert_severity = models.CharField(
max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO max_length=30, choices=SEVERITY_CHOICES, default="info"
) )
email_alert = models.BooleanField(default=False) email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False) text_alert = models.BooleanField(default=False)
@@ -93,11 +129,9 @@ class AutomatedTask(BaseAuditModel):
# options sent to agent for task creation # options sent to agent for task creation
# general task settings # general task settings
task_type = models.CharField( task_type = models.CharField(
max_length=100, choices=TaskType.choices, default=TaskType.MANUAL max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
) )
win_task_name = models.CharField( win_task_name = models.CharField(max_length=255, null=True, blank=True)
max_length=255, unique=True, blank=True, default=generate_task_name
) # should be changed to unique=True
run_time_date = DateTimeField(null=True, blank=True) run_time_date = DateTimeField(null=True, blank=True)
expire_date = DateTimeField(null=True, blank=True) expire_date = DateTimeField(null=True, blank=True)
@@ -131,89 +165,144 @@ class AutomatedTask(BaseAuditModel):
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7 run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1) task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1)
# deprecated def __str__(self):
managed_by_policy = models.BooleanField(default=False)
# non-database property
task_result: "Union[TaskResult, Dict[None, None]]" = {}
def __str__(self) -> str:
return self.name return self.name
def save(self, *args, **kwargs) -> None: def save(self, *args, **kwargs):
from autotasks.tasks import modify_win_task
from automation.tasks import update_policy_autotasks_fields_task
# if task is a policy task clear cache on everything # get old agent if exists
if self.policy:
cache.delete_many_pattern("site_*_tasks")
cache.delete_many_pattern("agent_*_tasks")
# get old task if exists
old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs) super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
# check if fields were updated that require a sync to the agent and set status to notsynced # check if fields were updated that require a sync to the agent
update_agent = False
if old_task: if old_task:
for field in self.fields_that_trigger_task_update_on_agent: for field in self.fields_that_trigger_task_update_on_agent:
if getattr(self, field) != getattr(old_task, field): if getattr(self, field) != getattr(old_task, field):
if self.policy: update_agent = True
TaskResult.objects.exclude( break
sync_status=TaskSyncStatus.INITIAL
).filter(task__policy_id=self.policy.id).update( # check if automated task was enabled/disabled and send celery task
sync_status=TaskSyncStatus.NOT_SYNCED if old_task and old_task.agent and update_agent:
modify_win_task.delay(pk=self.pk)
# check if policy task was edited and then check if it was a field worth copying to rest of agent tasks
elif old_task and old_task.policy:
if update_agent:
update_policy_autotasks_fields_task.delay(
task=self.pk, update_agent=update_agent
) )
else: else:
TaskResult.objects.filter(agent=self.agent, task=self).update( for field in self.policy_fields_to_copy:
sync_status=TaskSyncStatus.NOT_SYNCED if getattr(self, field) != getattr(old_task, field):
) update_policy_autotasks_fields_task.delay(task=self.pk)
break
def delete(self, *args, **kwargs):
# if task is a policy task clear cache on everything
if self.policy:
cache.delete_many_pattern("site_*_tasks")
cache.delete_many_pattern("agent_*_tasks")
super(AutomatedTask, self).delete(
*args,
**kwargs,
)
@property @property
def schedule(self) -> Optional[str]: def schedule(self):
if self.task_type == TaskType.MANUAL: if self.task_type == "manual":
return "Manual" return "Manual"
elif self.task_type == TaskType.CHECK_FAILURE: elif self.task_type == "checkfailure":
return "Every time check fails" return "Every time check fails"
elif self.task_type == TaskType.RUN_ONCE: elif self.task_type == "runonce":
return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}' return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}'
elif self.task_type == TaskType.DAILY: elif self.task_type == "daily":
run_time_nice = self.run_time_date.strftime("%I:%M%p") run_time_nice = self.run_time_date.strftime("%I:%M%p")
if self.daily_interval == 1: if self.daily_interval == 1:
return f"Daily at {run_time_nice}" return f"Daily at {run_time_nice}"
else: else:
return f"Every {self.daily_interval} days at {run_time_nice}" return f"Every {self.daily_interval} days at {run_time_nice}"
elif self.task_type == TaskType.WEEKLY: elif self.task_type == "weekly":
run_time_nice = self.run_time_date.strftime("%I:%M%p") run_time_nice = self.run_time_date.strftime("%I:%M%p")
days = bitdays_to_string(self.run_time_bit_weekdays) days = bitdays_to_string(self.run_time_bit_weekdays)
if self.weekly_interval != 1: if self.weekly_interval != 1:
return f"{days} at {run_time_nice}" return f"{days} at {run_time_nice}"
else: else:
return f"{days} at {run_time_nice} every {self.weekly_interval} weeks" return f"{days} at {run_time_nice} every {self.weekly_interval} weeks"
elif self.task_type == TaskType.MONTHLY: elif self.task_type == "monthly":
run_time_nice = self.run_time_date.strftime("%I:%M%p") run_time_nice = self.run_time_date.strftime("%I:%M%p")
months = bitmonths_to_string(self.monthly_months_of_year) months = bitmonths_to_string(self.monthly_months_of_year)
days = bitmonthdays_to_string(self.monthly_days_of_month) days = bitmonthdays_to_string(self.monthly_days_of_month)
return f"Runs on {months} on days {days} at {run_time_nice}" return f"Runs on {months} on days {days} at {run_time_nice}"
elif self.task_type == TaskType.MONTHLY_DOW: elif self.task_type == "monthlydow":
run_time_nice = self.run_time_date.strftime("%I:%M%p") run_time_nice = self.run_time_date.strftime("%I:%M%p")
months = bitmonths_to_string(self.monthly_months_of_year) months = bitmonths_to_string(self.monthly_months_of_year)
weeks = bitweeks_to_string(self.monthly_weeks_of_month) weeks = bitweeks_to_string(self.monthly_weeks_of_month)
days = bitdays_to_string(self.run_time_bit_weekdays) days = bitdays_to_string(self.run_time_bit_weekdays)
return f"Runs on {months} on {weeks} on {days} at {run_time_nice}" return f"Runs on {months} on {weeks} on {days} at {run_time_nice}"
@property
def last_run_as_timezone(self):
if self.last_run is not None and self.agent is not None:
return self.last_run.astimezone(
pytz.timezone(self.agent.timezone)
).strftime("%b-%d-%Y - %H:%M")
return self.last_run
# These fields will be duplicated on the agent tasks that are managed by a policy
@property
def policy_fields_to_copy(self) -> List[str]:
return [
"alert_severity",
"email_alert",
"text_alert",
"dashboard_alert",
"assigned_check",
"name",
"actions",
"run_time_bit_weekdays",
"run_time_date",
"expire_date",
"daily_interval",
"weekly_interval",
"task_type",
"win_task_name",
"enabled",
"remove_if_not_scheduled",
"run_asap_after_missed",
"custom_field",
"collector_all_output",
"monthly_days_of_month",
"monthly_months_of_year",
"monthly_weeks_of_month",
"task_repetition_duration",
"task_repetition_interval",
"stop_task_at_duration_end",
"random_task_delay",
"run_asap_after_missed",
"task_instance_policy",
"continue_on_error",
]
@property @property
def fields_that_trigger_task_update_on_agent(self) -> List[str]: def fields_that_trigger_task_update_on_agent(self) -> List[str]:
return FIELDS_TRIGGER_TASK_UPDATE_AGENT return [
"run_time_bit_weekdays",
"run_time_date",
"expire_date",
"daily_interval",
"weekly_interval",
"enabled",
"remove_if_not_scheduled",
"run_asap_after_missed",
"monthly_days_of_month",
"monthly_months_of_year",
"monthly_weeks_of_month",
"task_repetition_duration",
"task_repetition_interval",
"stop_task_at_duration_end",
"random_task_delay",
"run_asap_after_missed",
"task_instance_policy",
]
@staticmethod
def generate_task_name():
chars = string.ascii_letters
return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35))
@staticmethod @staticmethod
def serialize(task): def serialize(task):
@@ -222,35 +311,53 @@ class AutomatedTask(BaseAuditModel):
return TaskAuditSerializer(task).data return TaskAuditSerializer(task).data
def create_policy_task( def create_policy_task(self, agent=None, policy=None, assigned_check=None):
self, policy: "Policy", assigned_check: "Optional[Check]" = None
) -> None: # added to allow new policy tasks to be assigned to check only when the agent check exists already
### Copies certain properties on this task (self) to a new task and sets it to the supplied Policy if (
fields_to_copy = POLICY_TASK_FIELDS_TO_COPY self.assigned_check
and agent
and agent.agentchecks.filter(parent_check=self.assigned_check.id).exists()
):
assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.id)
# if policy is present, then this task is being copied to another policy
# if agent is present, then this task is being created on an agent from a policy
# exit if neither are set or if both are set
# also exit if assigned_check is set because this task will be created when the check is
if (
(not agent and not policy)
or (agent and policy)
or (self.assigned_check and not assigned_check)
):
return
task = AutomatedTask.objects.create( task = AutomatedTask.objects.create(
agent=agent,
policy=policy, policy=policy,
managed_by_policy=bool(agent),
parent_task=(self.pk if agent else None),
assigned_check=assigned_check, assigned_check=assigned_check,
) )
for field in fields_to_copy: for field in self.policy_fields_to_copy:
if field != "assigned_check":
setattr(task, field, getattr(self, field)) setattr(task, field, getattr(self, field))
task.save() task.save()
if agent:
task.create_task_on_agent()
# agent version >= 1.8.0 # agent version >= 1.8.0
def generate_nats_task_payload( def generate_nats_task_payload(self, editing=False):
self, agent: "Optional[Agent]" = None, editing: bool = False
) -> Dict[str, Any]:
task = { task = {
"pk": self.pk, "pk": self.pk,
"type": "rmm", "type": "rmm",
"name": self.win_task_name, "name": self.win_task_name,
"overwrite_task": editing, "overwrite_task": editing,
"enabled": self.enabled, "enabled": self.enabled,
"trigger": self.task_type "trigger": self.task_type if self.task_type != "checkfailure" else "manual",
if self.task_type != TaskType.CHECK_FAILURE
else TaskType.MANUAL,
"multiple_instances": self.task_instance_policy "multiple_instances": self.task_instance_policy
if self.task_instance_policy if self.task_instance_policy
else 0, else 0,
@@ -258,29 +365,11 @@ class AutomatedTask(BaseAuditModel):
if self.expire_date if self.expire_date
else False, else False,
"start_when_available": self.run_asap_after_missed "start_when_available": self.run_asap_after_missed
if self.task_type != TaskType.RUN_ONCE if self.task_type != "runonce"
else True, else True,
} }
if self.task_type in [ if self.task_type in ["runonce", "daily", "weekly", "monthly", "monthlydow"]:
TaskType.RUN_ONCE,
TaskType.DAILY,
TaskType.WEEKLY,
TaskType.MONTHLY,
TaskType.MONTHLY_DOW,
]:
# set runonce task in future if creating and run_asap_after_missed is set
if (
not editing
and self.task_type == TaskType.RUN_ONCE
and self.run_asap_after_missed
and agent
and self.run_time_date
< djangotime.now().astimezone(pytz.timezone(agent.timezone))
):
self.run_time_date = (
djangotime.now() + djangotime.timedelta(minutes=5)
).astimezone(pytz.timezone(agent.timezone))
task["start_year"] = int(self.run_time_date.strftime("%Y")) task["start_year"] = int(self.run_time_date.strftime("%Y"))
task["start_month"] = int(self.run_time_date.strftime("%-m")) task["start_month"] = int(self.run_time_date.strftime("%-m"))
@@ -307,14 +396,14 @@ class AutomatedTask(BaseAuditModel):
) )
task["stop_at_duration_end"] = self.stop_task_at_duration_end task["stop_at_duration_end"] = self.stop_task_at_duration_end
if self.task_type == TaskType.DAILY: if self.task_type == "daily":
task["day_interval"] = self.daily_interval task["day_interval"] = self.daily_interval
elif self.task_type == TaskType.WEEKLY: elif self.task_type == "weekly":
task["week_interval"] = self.weekly_interval task["week_interval"] = self.weekly_interval
task["days_of_week"] = self.run_time_bit_weekdays task["days_of_week"] = self.run_time_bit_weekdays
elif self.task_type == TaskType.MONTHLY: elif self.task_type == "monthly":
# check if "last day is configured" # check if "last day is configured"
if self.monthly_days_of_month >= 0x80000000: if self.monthly_days_of_month >= 0x80000000:
@@ -326,152 +415,222 @@ class AutomatedTask(BaseAuditModel):
task["months_of_year"] = self.monthly_months_of_year task["months_of_year"] = self.monthly_months_of_year
elif self.task_type == TaskType.MONTHLY_DOW: elif self.task_type == "monthlydow":
task["days_of_week"] = self.run_time_bit_weekdays task["days_of_week"] = self.run_time_bit_weekdays
task["months_of_year"] = self.monthly_months_of_year task["months_of_year"] = self.monthly_months_of_year
task["weeks_of_month"] = self.monthly_weeks_of_month task["weeks_of_month"] = self.monthly_weeks_of_month
return task return task
def create_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: def create_task_on_agent(self):
if self.policy and not agent: from agents.models import Agent
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try: agent = (
task_result = TaskResult.objects.get(agent=agent, task=self) Agent.objects.filter(pk=self.agent.pk)
except TaskResult.DoesNotExist: .only("pk", "version", "hostname", "agent_id")
task_result = TaskResult(agent=agent, task=self) .get()
task_result.save() )
if pyver.parse(agent.version) >= pyver.parse("1.8.0"):
nats_data = {
"func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(),
}
else:
if self.task_type == "scheduled":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "weekly",
"weekdays": self.run_time_bit_weekdays,
"pk": self.pk,
"name": self.win_task_name,
"hour": dt.datetime.strptime(
self.run_time_minute, "%H:%M"
).hour,
"min": dt.datetime.strptime(
self.run_time_minute, "%H:%M"
).minute,
},
}
elif self.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(agent.timezone)
task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
pytz.utc
)
now = djangotime.now()
if task_time_utc < now:
self.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
self.save(update_fields=["run_time_date"])
nats_data = { nats_data = {
"func": "schedtask", "func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(agent), "schedtaskpayload": {
"type": "rmm",
"trigger": "once",
"pk": self.pk,
"name": self.win_task_name,
"year": int(dt.datetime.strftime(self.run_time_date, "%Y")),
"month": dt.datetime.strftime(self.run_time_date, "%B"),
"day": int(dt.datetime.strftime(self.run_time_date, "%d")),
"hour": int(dt.datetime.strftime(self.run_time_date, "%H")),
"min": int(dt.datetime.strftime(self.run_time_date, "%M")),
},
} }
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5)) if self.run_asap_after_missed:
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if self.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif self.task_type == "checkfailure" or self.task_type == "manual":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "manual",
"pk": self.pk,
"name": self.win_task_name,
},
}
else:
return "error"
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok": if r != "ok":
task_result.sync_status = TaskSyncStatus.INITIAL self.sync_status = "initial"
task_result.save(update_fields=["sync_status"]) self.save(update_fields=["sync_status"])
DebugLog.warning( DebugLog.warning(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}. It will be created when the agent checks in.", message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.",
) )
return "timeout" return "timeout"
else: else:
task_result.sync_status = TaskSyncStatus.SYNCED self.sync_status = "synced"
task_result.save(update_fields=["sync_status"]) self.save(update_fields=["sync_status"])
DebugLog.info( DebugLog.info(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} was successfully created", message=f"{agent.hostname} task {self.name} was successfully created",
) )
return "ok" return "ok"
def modify_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: def modify_task_on_agent(self):
if self.policy and not agent: from agents.models import Agent
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try: agent = (
task_result = TaskResult.objects.get(agent=agent, task=self) Agent.objects.filter(pk=self.agent.pk)
except TaskResult.DoesNotExist: .only("pk", "version", "hostname", "agent_id")
task_result = TaskResult(agent=agent, task=self) .get()
task_result.save() )
if pyver.parse(agent.version) >= pyver.parse("1.8.0"):
nats_data = { nats_data = {
"func": "schedtask", "func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(editing=True), "schedtaskpayload": self.generate_nats_task_payload(editing=True),
} }
else:
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5)) nats_data = {
"func": "enableschedtask",
"schedtaskpayload": {
"name": self.win_task_name,
"enabled": self.enabled,
},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok": if r != "ok":
task_result.sync_status = TaskSyncStatus.NOT_SYNCED self.sync_status = "notsynced"
task_result.save(update_fields=["sync_status"]) self.save(update_fields=["sync_status"])
DebugLog.warning( DebugLog.warning(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}({task_result.agent.agent_id}). It will try again on next agent checkin", message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin",
) )
return "timeout" return "timeout"
else: else:
task_result.sync_status = TaskSyncStatus.SYNCED self.sync_status = "synced"
task_result.save(update_fields=["sync_status"]) self.save(update_fields=["sync_status"])
DebugLog.info( DebugLog.info(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} was successfully modified", message=f"{agent.hostname} task {self.name} was successfully modified",
) )
return "ok" return "ok"
def delete_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: def delete_task_on_agent(self):
if self.policy and not agent: from agents.models import Agent
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try: agent = (
task_result = TaskResult.objects.get(agent=agent, task=self) Agent.objects.filter(pk=self.agent.pk)
except TaskResult.DoesNotExist: .only("pk", "version", "hostname", "agent_id")
task_result = TaskResult(agent=agent, task=self) .get()
task_result.save() )
nats_data = { nats_data = {
"func": "delschedtask", "func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name}, "schedtaskpayload": {"name": self.win_task_name},
} }
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10)) r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r: if r != "ok" and "The system cannot find the file specified" not in r:
task_result.sync_status = TaskSyncStatus.PENDING_DELETION self.sync_status = "pendingdeletion"
try: try:
task_result.save(update_fields=["sync_status"]) self.save(update_fields=["sync_status"])
except DatabaseError: except DatabaseError:
pass pass
DebugLog.warning( DebugLog.warning(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin", message=f"{agent.hostname} task {self.name} will be deleted on next checkin",
) )
return "timeout" return "timeout"
else: else:
self.delete() self.delete()
DebugLog.info( DebugLog.info(
agent=agent, agent=agent,
log_type=DebugLogType.AGENT_ISSUES, log_type="agent_issues",
message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted", message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted",
) )
return "ok" return "ok"
def run_win_task(self, agent: "Optional[Agent]" = None) -> str: def run_win_task(self):
if self.policy and not agent: from agents.models import Agent
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try: agent = (
task_result = TaskResult.objects.get(agent=agent, task=self) Agent.objects.filter(pk=self.agent.pk)
except TaskResult.DoesNotExist: .only("pk", "version", "hostname", "agent_id")
task_result = TaskResult(agent=agent, task=self) .get()
task_result.save() )
asyncio.run( asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
task_result.agent.nats_cmd(
{"func": "runtask", "taskpk": self.pk}, wait=False
)
)
return "ok" return "ok"
def save_collector_results(self):
agent_field = self.custom_field.get_or_create_field_value(self.agent)
value = (
self.stdout.strip()
if self.collector_all_output
else self.stdout.strip().split("\n")[-1].strip()
)
agent_field.save_to_field(value)
def should_create_alert(self, alert_template=None): def should_create_alert(self, alert_template=None):
return ( return (
self.dashboard_alert self.dashboard_alert
@@ -487,64 +646,10 @@ class AutomatedTask(BaseAuditModel):
) )
) )
class TaskResult(models.Model):
class Meta:
unique_together = (("agent", "task"),)
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="taskresults",
on_delete=models.CASCADE,
)
task = models.ForeignKey(
"autotasks.AutomatedTask",
related_name="taskresults",
on_delete=models.CASCADE,
)
retcode = models.BigIntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
execution_time = models.CharField(max_length=100, default="0.0000")
last_run = models.DateTimeField(null=True, blank=True)
status = models.CharField(
max_length=30, choices=TaskStatus.choices, default=TaskStatus.PENDING
)
sync_status = models.CharField(
max_length=100, choices=TaskSyncStatus.choices, default=TaskSyncStatus.INITIAL
)
def __str__(self):
return f"{self.agent.hostname} - {self.task}"
def get_or_create_alert_if_needed(
self, alert_template: "Optional[AlertTemplate]"
) -> "Optional[Alert]":
from alerts.models import Alert
return Alert.create_or_return_task_alert(
self.task,
agent=self.agent,
skip_create=not self.task.should_create_alert(alert_template),
)
def save_collector_results(self) -> None:
agent_field = self.task.custom_field.get_or_create_field_value(self.agent)
value = (
self.stdout.strip()
if self.task.collector_all_output
else self.stdout.strip().split("\n")[-1].strip()
)
agent_field.save_to_field(value)
def send_email(self): def send_email(self):
CORE = get_core_settings() from core.models import CoreSettings
CORE = CoreSettings.objects.first()
# Format of Email sent when Task has email alert # Format of Email sent when Task has email alert
if self.agent: if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
@@ -556,11 +661,12 @@ class TaskResult(models.Model):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
) )
CORE.send_mail(subject, body, self.agent.alert_template) CORE.send_mail(subject, body, self.agent.alert_template) # type: ignore
def send_sms(self): def send_sms(self):
CORE = get_core_settings() from core.models import CoreSettings
CORE = CoreSettings.objects.first()
# Format of SMS sent when Task has SMS alert # Format of SMS sent when Task has SMS alert
if self.agent: if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
@@ -572,24 +678,27 @@ class TaskResult(models.Model):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
) )
CORE.send_sms(body, alert_template=self.agent.alert_template) CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore
def send_resolved_email(self): def send_resolved_email(self):
CORE = get_core_settings() from core.models import CoreSettings
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = ( body = (
subject subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
) )
CORE.send_mail(subject, body, alert_template=self.agent.alert_template) CORE.send_mail(subject, body, alert_template=self.agent.alert_template) # type: ignore
def send_resolved_sms(self): def send_resolved_sms(self):
CORE = get_core_settings() from core.models import CoreSettings
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = ( body = (
subject subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
) )
CORE.send_sms(body, alert_template=self.agent.alert_template) CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore

View File

@@ -4,7 +4,7 @@ from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class AutoTaskPerms(permissions.BasePermission): class AutoTaskPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
if r.method == "GET": if r.method == "GET":
if "agent_id" in view.kwargs.keys(): if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent( return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent(
@@ -17,5 +17,5 @@ class AutoTaskPerms(permissions.BasePermission):
class RunAutoTaskPerms(permissions.BasePermission): class RunAutoTaskPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view):
return _has_perm(r, "can_run_autotasks") return _has_perm(r, "can_run_autotasks")

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