Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8554cb5d6c | ||
|
|
f901614056 | ||
|
|
b555d217ab | ||
|
|
775c600234 | ||
|
|
128f2570b8 | ||
|
|
3cd53e79b4 | ||
|
|
ebba84ffda | ||
|
|
1e1a42fe98 | ||
|
|
8a744a440d | ||
|
|
f4fc3c7d55 | ||
|
|
0594d121de | ||
|
|
12c85d6234 | ||
|
|
5e37728f66 | ||
|
|
e8e19fede7 | ||
|
|
e565dbfa66 | ||
|
|
d180d6820c | ||
|
|
7f252e9b7c | ||
|
|
41db8681f8 | ||
|
|
26cd58fd6d | ||
|
|
63c7e1aa9d | ||
|
|
d5a6063e5e | ||
|
|
00affdbdec | ||
|
|
db3f0bbd4f | ||
|
|
020a59cb97 | ||
|
|
ff4fa6402d | ||
|
|
80f7555499 | ||
|
|
10cc187c5d | ||
|
|
def4a8a67e | ||
|
|
25843edb48 | ||
|
|
54294141b0 | ||
|
|
f3a8886b50 | ||
|
|
268cfaf234 | ||
|
|
651ae20304 | ||
|
|
e22f69a5dc | ||
|
|
a39808f44c | ||
|
|
fcb541f734 | ||
|
|
79ca0f1684 | ||
|
|
f2ebc38044 | ||
|
|
d4335675f1 | ||
|
|
be4b05423e | ||
|
|
d9fe8db2a7 | ||
|
|
f92e780765 | ||
|
|
7aebdb7c78 | ||
|
|
abb2dd842b | ||
|
|
75713c8015 | ||
|
|
42e1717455 | ||
|
|
bfb19a9eb7 | ||
|
|
3e08585114 | ||
|
|
12e82c7a8d | ||
|
|
0fcc683903 | ||
|
|
ed7a8dc0f5 | ||
|
|
0a9d29c98d | ||
|
|
f63e801608 | ||
|
|
77f04e1a32 | ||
|
|
362819ce16 | ||
|
|
1d9165a627 | ||
|
|
7ee8aaa027 | ||
|
|
516e279fc3 | ||
|
|
880611eddb | ||
|
|
c4bf776069 | ||
|
|
097d6464c0 | ||
|
|
b86e4e017f | ||
|
|
bbec17d498 | ||
|
|
3b7b5f4ec3 | ||
|
|
0986efef29 | ||
|
|
06091cbf1c | ||
|
|
b588bab268 | ||
|
|
0736cfe959 | ||
|
|
400352254a | ||
|
|
259c3dc781 | ||
|
|
506055a815 | ||
|
|
3edf6c57ba | ||
|
|
c404ae7ac8 | ||
|
|
312774e472 | ||
|
|
c540f802b0 | ||
|
|
6a2a2761e1 | ||
|
|
2508458c80 | ||
|
|
025d9e0141 | ||
|
|
734b3b07ab | ||
|
|
e4250a857a | ||
|
|
56d1b2716c | ||
|
|
c5d7e61e6c | ||
|
|
6222a127bd | ||
|
|
f0b7e515b6 | ||
|
|
98d8c23868 | ||
|
|
978bb9afd0 | ||
|
|
058598b5f3 | ||
|
|
5b7ab3a10f | ||
|
|
e42243c78b | ||
|
|
c650ee8498 | ||
|
|
50f8968901 | ||
|
|
b0fa2e6d80 | ||
|
|
d59589425e | ||
|
|
6c810e514b | ||
|
|
efa41dbd22 | ||
|
|
f34bcfd56d | ||
|
|
8ff2e3fb29 | ||
|
|
033c04a0f2 | ||
|
|
6ae2da22c1 | ||
|
|
cef1ab9512 | ||
|
|
94f02bfca3 | ||
|
|
a941bb1744 | ||
|
|
6ff591427a | ||
|
|
809e172280 | ||
|
|
17aedae0a9 | ||
|
|
ef817ccb3a | ||
|
|
0fb55b0bee | ||
|
|
a1a6eddc31 | ||
|
|
ff3d0b6b57 | ||
|
|
dd64cef4c4 | ||
|
|
9796848079 | ||
|
|
fea7eb4312 | ||
|
|
c12cd0e755 | ||
|
|
d86a72f858 | ||
|
|
50cd7f219a | ||
|
|
8252b3eccc | ||
|
|
d0c6e3a158 | ||
|
|
1505fa547e | ||
|
|
9017bad884 | ||
|
|
2ac5e316a5 | ||
|
|
29f9113062 | ||
|
|
46349672d8 | ||
|
|
4787be2db0 | ||
|
|
f0a8c5d732 | ||
|
|
9ad520bf7c | ||
|
|
bd0cc51554 | ||
|
|
12f599f974 | ||
|
|
0118d5fb40 | ||
|
|
65cadb311a | ||
|
|
dd75bd197d | ||
|
|
7e155bdb43 | ||
|
|
993b6fddf4 | ||
|
|
6ba51df6a7 | ||
|
|
1185ac58e1 | ||
|
|
f835997f49 | ||
|
|
a597dba775 | ||
|
|
3194e83a66 | ||
|
|
096c3cdd34 | ||
|
|
3a1ea42333 | ||
|
|
64877d4299 | ||
|
|
e957dc5e2c | ||
|
|
578d5c5830 | ||
|
|
96284f9508 | ||
|
|
698b38dcba | ||
|
|
6db826befe | ||
|
|
1a3d412d73 | ||
|
|
b8461c9dd8 | ||
|
|
699bd9de10 | ||
|
|
54b6866e21 | ||
|
|
afd155e9c1 | ||
|
|
910a717230 | ||
|
|
70fbd33d61 | ||
|
|
2da0d5ee21 | ||
|
|
98f64e057a | ||
|
|
3d9d936c56 | ||
|
|
2b4cb59df8 | ||
|
|
9d80da52e3 | ||
|
|
fd176d2c64 | ||
|
|
538b6de36b | ||
|
|
f7eca8aee0 | ||
|
|
a754d94c2c | ||
|
|
5e3493e6a9 | ||
|
|
619a14c26b | ||
|
|
7d9a8decf0 | ||
|
|
d11e14ad89 | ||
|
|
69189cf2af | ||
|
|
6e7d2f19d2 | ||
|
|
d99ebf5d6a | ||
|
|
ef2d19e95b | ||
|
|
e3a66f017e | ||
|
|
9e544ad471 | ||
|
|
5f19aa527a | ||
|
|
bfd5bc5c26 | ||
|
|
2d0ec3accd | ||
|
|
0999d98225 | ||
|
|
d8dd3e133f | ||
|
|
528470c37f | ||
|
|
c03cd53853 | ||
|
|
b57fc8a29c | ||
|
|
a04ed5c3ca | ||
|
|
3ad1df14f6 | ||
|
|
d8caf12fdc | ||
|
|
5ca9d30d5f | ||
|
|
a7a71b4a46 | ||
|
|
638603ac6b | ||
|
|
1d70c15027 | ||
|
|
7a5f03d672 | ||
|
|
653c482ff7 |
@@ -26,7 +26,8 @@ services:
|
||||
container_name: trmm-app-dev
|
||||
image: node:16-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||
user: 1000:1000
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
@@ -146,7 +146,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
|
||||
webenv="$(cat << EOF
|
||||
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
|
||||
APP_URL = "https://${APP_HOST}"
|
||||
DEV_PORT = ${APP_PORT}
|
||||
DOCKER_BUILD = 1
|
||||
EOF
|
||||
)"
|
||||
|
||||
@@ -1,41 +1,3 @@
|
||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
||||
asgiref==3.5.0
|
||||
celery==5.2.6
|
||||
channels==3.0.4
|
||||
channels_redis==3.4.0
|
||||
daphne==3.0.2
|
||||
Django==4.0.4
|
||||
django-cors-headers==3.11.0
|
||||
django-ipware==4.0.2
|
||||
django-rest-knox==4.2.0
|
||||
djangorestframework==3.13.1
|
||||
future==0.18.2
|
||||
msgpack==1.0.3
|
||||
nats-py==2.1.0
|
||||
packaging==21.3
|
||||
psycopg2-binary==2.9.3
|
||||
pycryptodome==3.14.1
|
||||
pyotp==2.6.0
|
||||
pytz==2022.1
|
||||
qrcode==7.3.1
|
||||
redis==4.2.2
|
||||
requests==2.27.1
|
||||
twilio==7.8.1
|
||||
urllib3==1.26.9
|
||||
validators==0.18.2
|
||||
websockets==10.2
|
||||
drf_spectacular==0.22.0
|
||||
meshctrl==0.1.15
|
||||
hiredis==2.0.0
|
||||
|
||||
# dev
|
||||
black==22.3.0
|
||||
django-extensions==3.1.5
|
||||
isort==5.10.1
|
||||
mypy==0.942
|
||||
types-pytz==2021.3.6
|
||||
model-bakery==1.5.0
|
||||
coverage==6.3.2
|
||||
django-silk==4.3.0
|
||||
django-stubs==1.10.1
|
||||
djangorestframework-stubs==1.5.0
|
||||
-r ../api/tacticalrmm/requirements.txt
|
||||
-r ../api/tacticalrmm/requirements-dev.txt
|
||||
-r ../api/tacticalrmm/requirements-test.txt
|
||||
62
.github/workflows/ci-tests.yml
vendored
62
.github/workflows/ci-tests.yml
vendored
@@ -10,23 +10,36 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10.4']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup virtual env and install requirements
|
||||
- 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: |
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
|
||||
sudo -u postgres psql -c 'CREATE DATABASE pipeline'
|
||||
sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
|
||||
pwd
|
||||
rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env
|
||||
cd api
|
||||
python3.10 -m venv env
|
||||
source env/bin/activate
|
||||
cd tacticalrmm
|
||||
python --version
|
||||
SETTINGS_FILE="tacticalrmm/settings.py"
|
||||
SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
|
||||
@@ -35,28 +48,25 @@ jobs:
|
||||
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: |
|
||||
cd api/tacticalrmm
|
||||
source ../env/bin/activate
|
||||
rm -f .coverage coverage.lcov
|
||||
pytest
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Codestyle black
|
||||
run: |
|
||||
cd api
|
||||
source env/bin/activate
|
||||
black --exclude migrations/ --check tacticalrmm
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: codecov/codecov-action@v2
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
directory: ./api/tacticalrmm
|
||||
files: ./api/tacticalrmm/coverage.xml
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript', 'python' ]
|
||||
language: [ 'go', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
|
||||
34
.github/workflows/devskim-analysis.yml
vendored
34
.github/workflows/devskim-analysis.yml
vendored
@@ -1,34 +0,0 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: DevSkim
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
schedule:
|
||||
- cron: '19 5 * * 0'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run DevSkim scanner
|
||||
uses: microsoft/DevSkim-Action@v1
|
||||
|
||||
- name: Upload DevSkim scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: devskim-results.sarif
|
||||
23
.vscode/extensions.json
vendored
Normal file
23
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
149
.vscode/settings.json
vendored
149
.vscode/settings.json
vendored
@@ -1,86 +1,69 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
|
||||
"python.languageServer": "Pylance",
|
||||
"python.analysis.extraPaths": [
|
||||
"api/tacticalrmm",
|
||||
"api/env",
|
||||
],
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportUnusedImport": "error",
|
||||
"reportDuplicateImport": "error",
|
||||
"reportGeneralTypeIssues": "none"
|
||||
},
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"mypy.runUsingActiveInterpreter": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.linting.mypyArgs": [
|
||||
"--ignore-missing-imports",
|
||||
"--follow-imports=silent",
|
||||
"--show-column-numbers",
|
||||
"--strict"
|
||||
],
|
||||
"python.formatting.provider": "black",
|
||||
"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,
|
||||
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
|
||||
"python.languageServer": "Pylance",
|
||||
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportUnusedImport": "error",
|
||||
"reportDuplicateImport": "error",
|
||||
"reportGeneralTypeIssues": "none"
|
||||
},
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.linting.mypyArgs": [
|
||||
"--ignore-missing-imports",
|
||||
"--follow-imports=silent",
|
||||
"--show-column-numbers",
|
||||
"--strict"
|
||||
],
|
||||
"python.linting.ignorePatterns": [
|
||||
"**/site-packages/**/*.py",
|
||||
".vscode/*.py",
|
||||
"**env/**"
|
||||
],
|
||||
"python.formatting.provider": "black",
|
||||
"mypy.targets": ["api/tacticalrmm"],
|
||||
"mypy.runUsingActiveInterpreter": true,
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.formatOnSave": true,
|
||||
"files.watcherExclude": {
|
||||
"files.watcherExclude": {
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/": true,
|
||||
"/node_modules/**": true,
|
||||
"**/env/": true,
|
||||
"/env/**": true,
|
||||
"**/__pycache__": true,
|
||||
"/__pycache__/**": true,
|
||||
"**/.cache": true,
|
||||
"**/.eggs": true,
|
||||
"**/.ipynb_checkpoints": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/*.egg-info": true,
|
||||
"**/*.feather": true,
|
||||
"**/*.parquet*": true,
|
||||
"**/*.pyc": true,
|
||||
"**/*.zip": true
|
||||
},
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/": true,
|
||||
"/node_modules/**": true,
|
||||
"**/env/": true,
|
||||
"/env/**": true,
|
||||
"**/__pycache__": true,
|
||||
"/__pycache__/**": true,
|
||||
"**/.cache": true,
|
||||
"**/.eggs": true,
|
||||
"**/.ipynb_checkpoints": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/*.egg-info": true,
|
||||
"**/*.feather": true,
|
||||
"**/*.parquet*": true,
|
||||
"**/*.pyc": true,
|
||||
"**/*.zip": true
|
||||
}
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": false
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": false,
|
||||
},
|
||||
"editor.snippetSuggestions": "none",
|
||||
},
|
||||
"[go.mod]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": true,
|
||||
"completeUnimported": true,
|
||||
"staticcheck": true,
|
||||
},
|
||||
"mypy.targets": [
|
||||
"api/tacticalrmm"
|
||||
],
|
||||
"python.linting.ignorePatterns": [
|
||||
"**/site-packages/**/*.py",
|
||||
".vscode/*.py",
|
||||
"**env/**"
|
||||
]
|
||||
}
|
||||
"editor.snippetSuggestions": "none"
|
||||
},
|
||||
"[go.mod]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": true,
|
||||
"completeUnimported": true,
|
||||
"staticcheck": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Tactical RMM is a remote monitoring & management tool, 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)
|
||||
|
||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||
# [LIVE DEMO](https://demo.tacticalrmm.com/)
|
||||
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)
|
||||
@@ -33,7 +33,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
|
||||
- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022
|
||||
|
||||
## Linux agent versions supported
|
||||
- Any distro with systemd
|
||||
- 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
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.12.2 | :white_check_mark: |
|
||||
| < 0.12.2 | :x: |
|
||||
| 0.14.1 | :white_check_mark: |
|
||||
| < 0.14.1 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
3
ansible/README.md
Normal file
3
ansible/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### tacticalrmm ansible WIP
|
||||
|
||||
ansible role to setup a Debian 11 VM for tacticalrmm local development
|
||||
37
ansible/roles/trmm_dev/defaults/main.yml
Normal file
37
ansible/roles/trmm_dev/defaults/main.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
user: "tactical"
|
||||
python_ver: "3.10.4"
|
||||
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
|
||||
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
|
||||
scripts_repo: "https://github.com/amidaware/community-scripts.git"
|
||||
backend_dir: "/opt/trmm"
|
||||
frontend_dir: "/opt/trmm-web"
|
||||
scripts_dir: "/opt/community-scripts"
|
||||
trmm_dir: "/opt/trmm/api/tacticalrmm/tacticalrmm"
|
||||
settings_file: "{{ trmm_dir }}/settings.py"
|
||||
local_settings_file: "{{ trmm_dir }}/local_settings.py"
|
||||
|
||||
base_pkgs:
|
||||
- build-essential
|
||||
- curl
|
||||
- wget
|
||||
- dirmngr
|
||||
- gnupg
|
||||
- openssl
|
||||
- gcc
|
||||
- g++
|
||||
- make
|
||||
- ca-certificates
|
||||
- redis
|
||||
- git
|
||||
|
||||
python_pkgs:
|
||||
- zlib1g-dev
|
||||
- libncurses5-dev
|
||||
- libgdbm-dev
|
||||
- libnss3-dev
|
||||
- libssl-dev
|
||||
- libreadline-dev
|
||||
- libffi-dev
|
||||
- libsqlite3-dev
|
||||
- libbz2-dev
|
||||
25
ansible/roles/trmm_dev/files/nginx-default.conf
Normal file
25
ansible/roles/trmm_dev/files/nginx-default.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
worker_rlimit_nofile 1000000;
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 2048;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
types_hash_max_size 2048;
|
||||
server_names_hash_bucket_size 64;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
gzip on;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
20
ansible/roles/trmm_dev/files/vimrc.local
Normal file
20
ansible/roles/trmm_dev/files/vimrc.local
Normal file
@@ -0,0 +1,20 @@
|
||||
" This file loads the default vim options at the beginning and prevents
|
||||
" that they are being loaded again later. All other options that will be set,
|
||||
" are added, or overwrite the default settings. Add as many options as you
|
||||
" whish at the end of this file.
|
||||
|
||||
" Load the defaults
|
||||
source $VIMRUNTIME/defaults.vim
|
||||
|
||||
" Prevent the defaults from being loaded again later, if the user doesn't
|
||||
" have a local vimrc (~/.vimrc)
|
||||
let skip_defaults_vim = 1
|
||||
|
||||
|
||||
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
|
||||
" Add as many options as you whish
|
||||
|
||||
" Set the mouse mode to 'r'
|
||||
if has('mouse')
|
||||
set mouse=r
|
||||
endif
|
||||
253
ansible/roles/trmm_dev/tasks/main.yml
Normal file
253
ansible/roles/trmm_dev/tasks/main.yml
Normal file
@@ -0,0 +1,253 @@
|
||||
---
|
||||
- name: set mouse mode for vim
|
||||
tags: vim
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: vimrc.local
|
||||
dest: /etc/vim/vimrc.local
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: install base packages
|
||||
tags: base
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: "{{ item }}"
|
||||
state: present
|
||||
update_cache: yes
|
||||
with_items:
|
||||
- "{{ base_pkgs }}"
|
||||
|
||||
- name: install python prereqs
|
||||
tags: python
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- "{{ python_pkgs }}"
|
||||
|
||||
- name: get cpu core count
|
||||
tags: python
|
||||
ansible.builtin.command: nproc
|
||||
register: numprocs
|
||||
|
||||
- name: Create python tmpdir
|
||||
tags: python
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: python
|
||||
register: python_tmp
|
||||
|
||||
- name: download and extract python
|
||||
tags: python
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
|
||||
dest: "{{ python_tmp.path }}"
|
||||
remote_src: yes
|
||||
|
||||
- name: compile python
|
||||
tags: python
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
|
||||
cmd: |
|
||||
./configure --enable-optimizations
|
||||
make -j {{ numprocs.stdout }}
|
||||
|
||||
- name: alt install python
|
||||
tags: python
|
||||
become: yes
|
||||
ansible.builtin.shell:
|
||||
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
|
||||
cmd: |
|
||||
make altinstall
|
||||
|
||||
- name: install nginx
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: nginx
|
||||
state: present
|
||||
|
||||
- name: set nginx default conf
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
src: nginx-default.conf
|
||||
dest: /etc/nginx/nginx.conf
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: ensure nginx enabled and restarted
|
||||
tags: nginx
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: nginx
|
||||
enabled: yes
|
||||
state: restarted
|
||||
|
||||
- name: create postgres repo
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
content: "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main"
|
||||
dest: /etc/apt/sources.list.d/pgdg.list
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0440"
|
||||
|
||||
- name: import postgres repo signing key
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.apt_key:
|
||||
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
state: present
|
||||
|
||||
- name: install postgresql
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: postgresql-14
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: ensure postgres enabled and started
|
||||
tags: postgres
|
||||
become: yes
|
||||
ansible.builtin.service:
|
||||
name: postgresql
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: setup database
|
||||
tags: postgres
|
||||
become: yes
|
||||
become_user: postgres
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
psql -c "CREATE DATABASE tacticalrmm"
|
||||
psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
|
||||
psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
|
||||
psql -c "ALTER ROLE {{ db_user }} CREATEDB"
|
||||
psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
|
||||
|
||||
- name: create repo dirs
|
||||
become: yes
|
||||
tags: git
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
with_items:
|
||||
- "{{ backend_dir }}"
|
||||
- "{{ frontend_dir }}"
|
||||
- "{{ scripts_dir }}"
|
||||
|
||||
- name: git clone repos
|
||||
tags: git
|
||||
ansible.builtin.git:
|
||||
repo: "{{ item.repo }}"
|
||||
dest: "{{ item.dest }}"
|
||||
version: "{{ item.version }}"
|
||||
with_items:
|
||||
- {
|
||||
repo: "{{ backend_repo }}",
|
||||
dest: "{{ backend_dir }}",
|
||||
version: develop,
|
||||
}
|
||||
- {
|
||||
repo: "{{ frontend_repo }}",
|
||||
dest: "{{ frontend_dir }}",
|
||||
version: develop,
|
||||
}
|
||||
- { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
|
||||
|
||||
- name: get nats_server_ver
|
||||
tags: nats
|
||||
ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
|
||||
register: nats_server_ver
|
||||
|
||||
- name: Create nats tmpdir
|
||||
tags: nats
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: nats
|
||||
register: nats_tmp
|
||||
|
||||
- name: download and extract nats
|
||||
tags: nats
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64.tar.gz"
|
||||
dest: "{{ nats_tmp.path }}"
|
||||
remote_src: yes
|
||||
|
||||
- name: install nats
|
||||
tags: nats
|
||||
become: yes
|
||||
ansible.builtin.copy:
|
||||
remote_src: yes
|
||||
src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64/nats-server"
|
||||
dest: /usr/local/bin/nats-server
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Create nodejs tmpdir
|
||||
tags: nodejs
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: nodejs
|
||||
register: nodejs_tmp
|
||||
|
||||
- name: download nodejs setup
|
||||
tags: nodejs
|
||||
ansible.builtin.get_url:
|
||||
url: https://deb.nodesource.com/setup_16.x
|
||||
dest: "{{ nodejs_tmp.path }}/setup_node.sh"
|
||||
mode: "0755"
|
||||
|
||||
- name: run node setup script
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
|
||||
|
||||
- name: install nodejs
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.apt:
|
||||
pkg: nodejs
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: update npm
|
||||
tags: nodejs
|
||||
become: yes
|
||||
ansible.builtin.shell:
|
||||
cmd: npm install -g npm
|
||||
|
||||
- name: deploy django local settings
|
||||
tags: django
|
||||
ansible.builtin.template:
|
||||
src: local_settings.j2
|
||||
dest: "{{ local_settings_file }}"
|
||||
mode: "0644"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
|
||||
- name: remove tempdirs
|
||||
tags: cleanup
|
||||
become: yes
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- "{{ nats_tmp.path }}"
|
||||
- "{{ python_tmp.path }}"
|
||||
- "{{ nodejs_tmp.path }}"
|
||||
19
ansible/roles/trmm_dev/templates/local_settings.j2
Normal file
19
ansible/roles/trmm_dev/templates/local_settings.j2
Normal file
@@ -0,0 +1,19 @@
|
||||
SECRET_KEY = "{{ django_secret }}"
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['{{ api }}']
|
||||
ADMIN_URL = "admin/"
|
||||
CORS_ORIGIN_WHITELIST = [
|
||||
"https://{{ rmm }}"
|
||||
]
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'tacticalrmm',
|
||||
'USER': '{{ db_user }}',
|
||||
'PASSWORD': '{{ db_passwd }}',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
REDIS_HOST = "localhost"
|
||||
ADMIN_ENABLED = True
|
||||
14
ansible/roles/trmm_dev/vars/main.yml
Normal file
14
ansible/roles/trmm_dev/vars/main.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
api: 'api.example.com'
|
||||
rmm: 'rmm.example.com'
|
||||
mesh: 'mesh.example.com'
|
||||
github_username: 'changeme'
|
||||
github_email: 'changeme@example.com'
|
||||
mesh_site: 'changeme'
|
||||
mesh_user: 'changeme'
|
||||
mesh_token: 'changeme'
|
||||
db_user: 'changeme'
|
||||
db_passwd: 'changeme'
|
||||
django_secret: 'changeme'
|
||||
|
||||
|
||||
6
ansible/setup_dev.yml
Normal file
6
ansible/setup_dev.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- hosts: "{{ target }}"
|
||||
vars:
|
||||
ansible_user: tactical
|
||||
roles:
|
||||
- trmm_dev
|
||||
@@ -1,26 +1,15 @@
|
||||
[run]
|
||||
source = .
|
||||
[report]
|
||||
show_missing = True
|
||||
include = *.py
|
||||
omit =
|
||||
tacticalrmm/asgi.py
|
||||
tacticalrmm/wsgi.py
|
||||
manage.py
|
||||
*/__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
|
||||
/usr/local/lib/*
|
||||
**/migrations/*
|
||||
**/test*.py
|
||||
|
||||
[report]
|
||||
show_missing = True
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import uuid
|
||||
|
||||
from accounts.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates the installer user"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
def handle(self, *args, **kwargs): # type: ignore
|
||||
self.stdout.write("Checking if installer user has been created...")
|
||||
if User.objects.filter(is_installer_user=True).exists():
|
||||
self.stdout.write("Installer user already exists")
|
||||
return
|
||||
|
||||
User.objects.create_user( # type: ignore
|
||||
User.objects.create_user(
|
||||
username=uuid.uuid4().hex,
|
||||
is_installer_user=True,
|
||||
password=User.objects.make_random_password(60), # type: ignore
|
||||
password=User.objects.make_random_password(60),
|
||||
block_dashboard_login=True,
|
||||
)
|
||||
self.stdout.write("Installer user has been created")
|
||||
|
||||
@@ -6,7 +6,7 @@ from knox.models import AuthToken
|
||||
class Command(BaseCommand):
|
||||
help = "Deletes all knox web tokens"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
def handle(self, *args, **kwargs): # type: ignore
|
||||
# only delete web tokens, not any generated by the installer or deployments
|
||||
dont_delete = djangotime.now() + djangotime.timedelta(hours=23)
|
||||
tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import subprocess
|
||||
|
||||
import pyotp
|
||||
from accounts.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generates barcode for Authenticator and creates totp for user"
|
||||
|
||||
@@ -2,9 +2,10 @@ import os
|
||||
import subprocess
|
||||
|
||||
import pyotp
|
||||
from accounts.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Reset 2fa"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from accounts.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Reset password for user"
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models.fields import CharField, DateTimeField
|
||||
from django.core.cache import cache
|
||||
from logs.models import BaseAuditModel
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from tacticalrmm.constants import ROLE_CACHE_PREFIX
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models.fields import CharField, DateTimeField
|
||||
|
||||
AGENT_DBLCLICK_CHOICES = [
|
||||
("editagent", "Edit Agent"),
|
||||
("takecontrol", "Take Control"),
|
||||
("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"),
|
||||
]
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.constants import (
|
||||
ROLE_CACHE_PREFIX,
|
||||
AgentDblClick,
|
||||
AgentTableTabs,
|
||||
ClientTreeSort,
|
||||
)
|
||||
|
||||
|
||||
class User(AbstractUser, BaseAuditModel):
|
||||
@@ -33,8 +20,8 @@ class User(AbstractUser, BaseAuditModel):
|
||||
totp_key = models.CharField(max_length=50, null=True, blank=True)
|
||||
dark_mode = models.BooleanField(default=True)
|
||||
show_community_scripts = models.BooleanField(default=True)
|
||||
agent_dblclick_action = models.CharField(
|
||||
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
|
||||
agent_dblclick_action: "AgentDblClick" = models.CharField(
|
||||
max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT
|
||||
)
|
||||
url_action = models.ForeignKey(
|
||||
"core.URLAction",
|
||||
@@ -44,11 +31,11 @@ class User(AbstractUser, BaseAuditModel):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
default_agent_tbl_tab = models.CharField(
|
||||
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||
max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.SERVER
|
||||
)
|
||||
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
|
||||
client_tree_sort = models.CharField(
|
||||
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
|
||||
max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL
|
||||
)
|
||||
client_tree_splitter = models.PositiveIntegerField(default=11)
|
||||
loading_bar_color = models.CharField(max_length=255, default="red")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from accounts.models import APIKey, User
|
||||
from accounts.serializers import APIKeySerializer
|
||||
from django.test import override_settings
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from accounts.models import APIKey, User
|
||||
from accounts.serializers import APIKeySerializer
|
||||
from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
|
||||
@@ -283,9 +284,9 @@ class TestUserAction(TacticalTestCase):
|
||||
data = {
|
||||
"dark_mode": True,
|
||||
"show_community_scripts": True,
|
||||
"agent_dblclick_action": "editagent",
|
||||
"default_agent_tbl_tab": "mixed",
|
||||
"client_tree_sort": "alpha",
|
||||
"agent_dblclick_action": AgentDblClick.EDIT_AGENT,
|
||||
"default_agent_tbl_tab": AgentTableTabs.MIXED,
|
||||
"client_tree_sort": ClientTreeSort.ALPHA,
|
||||
"client_tree_splitter": 14,
|
||||
"loading_bar_color": "green",
|
||||
"clear_search_when_switching": False,
|
||||
@@ -338,7 +339,7 @@ class TestAPIKeyViews(TacticalTestCase):
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
apikey = APIKey.objects.get(pk=apikey.pk)
|
||||
self.assertEquals(apikey.name, "New Name")
|
||||
self.assertEqual(apikey.name, "New Name")
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ipware import get_client_ip
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
from logs.models import AuditLog
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
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 .permissions import AccountsPerms, APIKeyPerms, RolesPerms
|
||||
@@ -93,7 +93,7 @@ class LoginView(KnoxLoginView):
|
||||
login(request, user)
|
||||
|
||||
# save ip information
|
||||
client_ip, is_routable = get_client_ip(request)
|
||||
client_ip, _ = get_client_ip(request)
|
||||
user.last_login_ip = client_ip
|
||||
user.save()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
from itertools import cycle
|
||||
|
||||
@@ -8,10 +8,11 @@ from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery.recipe import Recipe, foreign_key, seq
|
||||
|
||||
from tacticalrmm.constants import AgentMonType, AgentPlat
|
||||
|
||||
def generate_agent_id(hostname):
|
||||
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
|
||||
return f"{rand}-{hostname}"
|
||||
|
||||
def generate_agent_id() -> str:
|
||||
return "".join(secrets.choice(string.ascii_letters) for i in range(39))
|
||||
|
||||
|
||||
site = Recipe("clients.Site")
|
||||
@@ -24,26 +25,34 @@ def get_wmi_data():
|
||||
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(
|
||||
"agents.Agent",
|
||||
site=foreign_key(site),
|
||||
hostname="DESKTOP-TEST123",
|
||||
version="1.3.0",
|
||||
monitoring_type=cycle(["workstation", "server"]),
|
||||
agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
|
||||
monitoring_type=cycle(AgentMonType.values),
|
||||
agent_id=seq(generate_agent_id()),
|
||||
last_seen=djangotime.now() - djangotime.timedelta(days=5),
|
||||
plat="windows",
|
||||
plat=AgentPlat.WINDOWS,
|
||||
)
|
||||
|
||||
server_agent = agent.extend(
|
||||
monitoring_type="server",
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
)
|
||||
|
||||
workstation_agent = agent.extend(
|
||||
monitoring_type="workstation",
|
||||
monitoring_type=AgentMonType.WORKSTATION,
|
||||
)
|
||||
|
||||
online_agent = agent.extend(last_seen=djangotime.now())
|
||||
online_agent = agent.extend(
|
||||
last_seen=djangotime.now(), services=get_win_svcs(), wmi_detail=get_wmi_data()
|
||||
)
|
||||
|
||||
offline_agent = agent.extend(
|
||||
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
|
||||
@@ -78,4 +87,4 @@ agent_with_services = agent.extend(
|
||||
],
|
||||
)
|
||||
|
||||
agent_with_wmi = agent.extend(wmi=get_wmi_data())
|
||||
agent_with_wmi = agent.extend(wmi_detail=get_wmi_data())
|
||||
|
||||
83
api/tacticalrmm/agents/consumers.py
Normal file
83
api/tacticalrmm/agents/consumers.py
Normal file
@@ -0,0 +1,83 @@
|
||||
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
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from agents.models import Agent
|
||||
from clients.models import Client, Site
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import asyncio
|
||||
|
||||
from agents.models import Agent
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone as djangotime
|
||||
from packaging import version as pyver
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.constants import AGENT_DEFER
|
||||
from tacticalrmm.utils import reload_nats
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# import datetime as dt
|
||||
import random
|
||||
|
||||
from agents.models import Agent
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from agents.models import Agent
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "stuff for demo site in cron"
|
||||
@@ -23,14 +24,6 @@ class Command(BaseCommand):
|
||||
rand = now - djangotime.timedelta(minutes=random.randint(10, 20))
|
||||
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")
|
||||
for agent in agents:
|
||||
agent.last_seen = random.choice(random_dates)
|
||||
|
||||
@@ -3,30 +3,54 @@ import json
|
||||
import random
|
||||
import string
|
||||
|
||||
from accounts.models import User
|
||||
from agents.models import Agent, AgentHistory
|
||||
from automation.models import Policy
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from checks.models import Check, CheckResult, CheckHistory
|
||||
from clients.models import Client, Site
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from accounts.models import User
|
||||
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 logs.models import AuditLog, PendingAction
|
||||
from scripts.models import Script
|
||||
from software.models import InstalledSoftware
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
|
||||
from tacticalrmm.constants import (
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
AgentPlat,
|
||||
AlertSeverity,
|
||||
CheckStatus,
|
||||
CheckType,
|
||||
EvtLogFailWhen,
|
||||
EvtLogNames,
|
||||
EvtLogTypes,
|
||||
PAAction,
|
||||
ScriptShell,
|
||||
TaskSyncStatus,
|
||||
TaskType,
|
||||
)
|
||||
from tacticalrmm.demo_data import (
|
||||
check_network_loc_aware_ps1,
|
||||
check_storage_pool_health_ps1,
|
||||
clear_print_spool_bat,
|
||||
disks,
|
||||
disks_linux_deb,
|
||||
disks_linux_pi,
|
||||
ping_fail_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 = 250
|
||||
AGENTS_TO_GENERATE = 20
|
||||
|
||||
SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
|
||||
WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json")
|
||||
@@ -43,19 +67,19 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath(
|
||||
class Command(BaseCommand):
|
||||
help = "populate database with fake agents"
|
||||
|
||||
def rand_string(self, length):
|
||||
def rand_string(self, length: int) -> str:
|
||||
chars = string.ascii_letters
|
||||
return "".join(random.choice(chars) for _ in range(length))
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
def handle(self, *args, **kwargs) -> None:
|
||||
|
||||
user = User.objects.first()
|
||||
if user:
|
||||
user.totp_key = "ABSA234234"
|
||||
user.save(update_fields=["totp_key"])
|
||||
|
||||
Client.objects.all().delete()
|
||||
Agent.objects.all().delete()
|
||||
Client.objects.all().delete()
|
||||
Check.objects.all().delete()
|
||||
Script.objects.all().delete()
|
||||
AutomatedTask.objects.all().delete()
|
||||
@@ -65,6 +89,9 @@ class Command(BaseCommand):
|
||||
PendingAction.objects.all().delete()
|
||||
|
||||
call_command("load_community_scripts")
|
||||
call_command("initial_db_setup")
|
||||
call_command("load_chocos")
|
||||
call_command("create_installer_user")
|
||||
|
||||
# policies
|
||||
check_policy = Policy()
|
||||
@@ -95,27 +122,27 @@ class Command(BaseCommand):
|
||||
update_policy.email_if_fail = True
|
||||
update_policy.save()
|
||||
|
||||
clients = [
|
||||
clients = (
|
||||
"Company 1",
|
||||
"Company 2",
|
||||
"Company 3",
|
||||
"Company 1",
|
||||
"Company 4",
|
||||
"Company 5",
|
||||
"Company 6",
|
||||
]
|
||||
sites1 = ["HQ1", "LA Office 1", "NY Office 1"]
|
||||
sites2 = ["HQ2", "LA Office 2", "NY Office 2"]
|
||||
sites3 = ["HQ3", "LA Office 3", "NY Office 3"]
|
||||
sites4 = ["HQ4", "LA Office 4", "NY Office 4"]
|
||||
sites5 = ["HQ5", "LA Office 5", "NY Office 5"]
|
||||
sites6 = ["HQ6", "LA Office 6", "NY Office 6"]
|
||||
)
|
||||
sites1 = ("HQ1", "LA Office 1", "NY Office 1")
|
||||
sites2 = ("HQ2", "LA Office 2", "NY Office 2")
|
||||
sites3 = ("HQ3", "LA Office 3", "NY Office 3")
|
||||
sites4 = ("HQ4", "LA Office 4", "NY Office 4")
|
||||
sites5 = ("HQ5", "LA Office 5", "NY Office 5")
|
||||
sites6 = ("HQ6", "LA Office 6", "NY Office 6")
|
||||
|
||||
client1 = Client(name="Company 1")
|
||||
client2 = Client(name="Company 2")
|
||||
client3 = Client(name="Company 3")
|
||||
client4 = Client(name="Company 4")
|
||||
client5 = Client(name="Company 5")
|
||||
client6 = Client(name="Company 6")
|
||||
client1 = Client(name=clients[0])
|
||||
client2 = Client(name=clients[1])
|
||||
client3 = Client(name=clients[2])
|
||||
client4 = Client(name=clients[3])
|
||||
client5 = Client(name=clients[4])
|
||||
client6 = Client(name=clients[5])
|
||||
|
||||
client1.save()
|
||||
client2.save()
|
||||
@@ -142,7 +169,7 @@ class Command(BaseCommand):
|
||||
for site in sites6:
|
||||
Site(client=client6, name=site).save()
|
||||
|
||||
hostnames = [
|
||||
hostnames = (
|
||||
"DC-1",
|
||||
"DC-2",
|
||||
"FSV-1",
|
||||
@@ -150,26 +177,30 @@ class Command(BaseCommand):
|
||||
"WSUS",
|
||||
"DESKTOP-12345",
|
||||
"LAPTOP-55443",
|
||||
]
|
||||
descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"]
|
||||
modes = ["server", "workstation"]
|
||||
op_systems_servers = [
|
||||
)
|
||||
descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
|
||||
modes = AgentMonType.values
|
||||
op_systems_servers = (
|
||||
"Microsoft Windows Server 2016 Standard, 64bit (build 14393)",
|
||||
"Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)",
|
||||
"Microsoft Windows Server 2019 Standard, 64bit (build 17763)",
|
||||
]
|
||||
)
|
||||
|
||||
op_systems_workstations = [
|
||||
op_systems_workstations = (
|
||||
"Microsoft Windows 8.1 Pro, 64bit (build 9600)",
|
||||
"Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)",
|
||||
"Microsoft Windows 10 Pro, 64bit (build 18363)",
|
||||
]
|
||||
)
|
||||
|
||||
public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
|
||||
linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64"
|
||||
linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
|
||||
|
||||
total_rams = [4, 8, 16, 32, 64, 128]
|
||||
public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
|
||||
|
||||
total_rams = (4, 8, 16, 32, 64, 128)
|
||||
|
||||
now = dt.datetime.now()
|
||||
django_now = djangotime.now()
|
||||
|
||||
boot_times = []
|
||||
|
||||
@@ -181,7 +212,7 @@ class Command(BaseCommand):
|
||||
rand_days = now - dt.timedelta(days=random.randint(2, 50))
|
||||
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:
|
||||
services = json.load(f)
|
||||
@@ -196,10 +227,7 @@ class Command(BaseCommand):
|
||||
with open(WMI_3) as f:
|
||||
wmi3 = json.load(f)
|
||||
|
||||
wmi_details = []
|
||||
wmi_details.append(wmi1)
|
||||
wmi_details.append(wmi2)
|
||||
wmi_details.append(wmi3)
|
||||
wmi_details = [i for i in (wmi1, wmi2, wmi3)]
|
||||
|
||||
# software
|
||||
with open(SW_1) as f:
|
||||
@@ -208,9 +236,7 @@ class Command(BaseCommand):
|
||||
with open(SW_2) as f:
|
||||
software2 = json.load(f)
|
||||
|
||||
softwares = []
|
||||
softwares.append(software1)
|
||||
softwares.append(software2)
|
||||
softwares = [i for i in (software1, software2)]
|
||||
|
||||
# windows updates
|
||||
with open(WIN_UPDATES) as f:
|
||||
@@ -226,74 +252,97 @@ class Command(BaseCommand):
|
||||
clear_spool.name = "Clear Print Spooler"
|
||||
clear_spool.description = "clears the print spooler. Fuck printers"
|
||||
clear_spool.filename = "clear_print_spool.bat"
|
||||
clear_spool.shell = "cmd"
|
||||
clear_spool.shell = ScriptShell.CMD
|
||||
clear_spool.script_body = clear_print_spool_bat
|
||||
clear_spool.save()
|
||||
|
||||
check_net_aware = Script()
|
||||
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.filename = "check_network_loc_aware.ps1"
|
||||
check_net_aware.shell = "powershell"
|
||||
check_net_aware.shell = ScriptShell.POWERSHELL
|
||||
check_net_aware.script_body = check_network_loc_aware_ps1
|
||||
check_net_aware.save()
|
||||
|
||||
check_pool_health = Script()
|
||||
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.filename = "check_storage_pool_health.ps1"
|
||||
check_pool_health.shell = "powershell"
|
||||
check_pool_health.shell = ScriptShell.POWERSHELL
|
||||
check_pool_health.script_body = check_storage_pool_health_ps1
|
||||
check_pool_health.save()
|
||||
|
||||
restart_nla = Script()
|
||||
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.filename = "restart_nla.ps1"
|
||||
restart_nla.shell = "powershell"
|
||||
restart_nla.shell = ScriptShell.POWERSHELL
|
||||
restart_nla.script_body = restart_nla_ps1
|
||||
restart_nla.save()
|
||||
|
||||
show_tmp_dir_script = Script()
|
||||
show_tmp_dir_script.name = "Check temp dir"
|
||||
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.shell = "python"
|
||||
show_tmp_dir_script.shell = ScriptShell.PYTHON
|
||||
show_tmp_dir_script.script_body = show_temp_dir_py
|
||||
show_tmp_dir_script.save()
|
||||
|
||||
for count_agents in range(AGENTS_TO_GENERATE):
|
||||
|
||||
client = random.choice(clients)
|
||||
|
||||
if client == "Company 1":
|
||||
if client == clients[0]:
|
||||
site = random.choice(sites1)
|
||||
elif client == "Company 2":
|
||||
elif client == clients[1]:
|
||||
site = random.choice(sites2)
|
||||
elif client == "Company 3":
|
||||
elif client == clients[2]:
|
||||
site = random.choice(sites3)
|
||||
elif client == "Company 4":
|
||||
elif client == clients[3]:
|
||||
site = random.choice(sites4)
|
||||
elif client == "Company 5":
|
||||
elif client == clients[4]:
|
||||
site = random.choice(sites5)
|
||||
elif client == "Company 6":
|
||||
elif client == clients[5]:
|
||||
site = random.choice(sites6)
|
||||
else:
|
||||
site = None
|
||||
|
||||
agent = Agent()
|
||||
|
||||
mode = random.choice(modes)
|
||||
if mode == "server":
|
||||
agent.operating_system = random.choice(op_systems_servers)
|
||||
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.operating_system = random.choice(op_systems_workstations)
|
||||
agent.plat = AgentPlat.WINDOWS
|
||||
agent.goarch = "amd64"
|
||||
mode = random.choice(modes)
|
||||
agent.wmi_detail = random.choice(wmi_details)
|
||||
agent.services = services
|
||||
agent.disks = random.choice(disks)
|
||||
if mode == AgentMonType.SERVER:
|
||||
agent.operating_system = random.choice(op_systems_servers)
|
||||
else:
|
||||
agent.operating_system = random.choice(op_systems_workstations)
|
||||
|
||||
agent.hostname = random.choice(hostnames)
|
||||
agent.version = settings.LATEST_AGENT_VER
|
||||
agent.site = Site.objects.get(name=site)
|
||||
agent.agent_id = self.rand_string(25)
|
||||
agent.agent_id = self.rand_string(40)
|
||||
agent.description = random.choice(descriptions)
|
||||
agent.monitoring_type = mode
|
||||
agent.public_ip = random.choice(public_ips)
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.plat = "windows"
|
||||
agent.plat_release = "windows-2019Server"
|
||||
agent.last_seen = django_now
|
||||
|
||||
agent.total_ram = random.choice(total_rams)
|
||||
agent.boot_time = random.choice(boot_times)
|
||||
agent.logged_in_username = random.choice(user_names)
|
||||
@@ -303,40 +352,36 @@ class Command(BaseCommand):
|
||||
agent.overdue_email_alert = random.choice([True, False])
|
||||
agent.overdue_text_alert = 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.save()
|
||||
|
||||
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
|
||||
if agent.plat == AgentPlat.WINDOWS:
|
||||
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
|
||||
|
||||
if mode == "workstation":
|
||||
if mode == AgentMonType.WORKSTATION:
|
||||
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
|
||||
else:
|
||||
WinUpdatePolicy(agent=agent).save()
|
||||
|
||||
# windows updates load
|
||||
guids = []
|
||||
for k in windows_updates.keys():
|
||||
guids.append(k)
|
||||
|
||||
for i in guids:
|
||||
WinUpdate(
|
||||
agent=agent,
|
||||
guid=i,
|
||||
kb=windows_updates[i]["KBs"][0],
|
||||
title=windows_updates[i]["Title"],
|
||||
installed=windows_updates[i]["Installed"],
|
||||
downloaded=windows_updates[i]["Downloaded"],
|
||||
description=windows_updates[i]["Description"],
|
||||
severity=windows_updates[i]["Severity"],
|
||||
).save()
|
||||
if agent.plat == AgentPlat.WINDOWS:
|
||||
# windows updates load
|
||||
guids = [i for i in windows_updates.keys()]
|
||||
for i in guids:
|
||||
WinUpdate(
|
||||
agent=agent,
|
||||
guid=i,
|
||||
kb=windows_updates[i]["KBs"][0],
|
||||
title=windows_updates[i]["Title"],
|
||||
installed=windows_updates[i]["Installed"],
|
||||
downloaded=windows_updates[i]["Downloaded"],
|
||||
description=windows_updates[i]["Description"],
|
||||
severity=windows_updates[i]["Severity"],
|
||||
).save()
|
||||
|
||||
# agent histories
|
||||
hist = AgentHistory()
|
||||
hist.agent = agent
|
||||
hist.type = "cmd_run"
|
||||
hist.type = AgentHistoryType.CMD_RUN
|
||||
hist.command = "ping google.com"
|
||||
hist.username = "demo"
|
||||
hist.results = ping_success_output
|
||||
@@ -344,7 +389,7 @@ class Command(BaseCommand):
|
||||
|
||||
hist1 = AgentHistory()
|
||||
hist1.agent = agent
|
||||
hist1.type = "script_run"
|
||||
hist1.type = AgentHistoryType.SCRIPT_RUN
|
||||
hist1.script = clear_spool
|
||||
hist1.script_results = {
|
||||
"id": 1,
|
||||
@@ -355,50 +400,57 @@ class Command(BaseCommand):
|
||||
}
|
||||
hist1.save()
|
||||
|
||||
# disk space check
|
||||
check1 = Check()
|
||||
check_result1 = CheckResult(assigned_check=check1, agent=agent)
|
||||
check1.agent = agent
|
||||
check1.check_type = "diskspace"
|
||||
check_result1.status = "passing"
|
||||
check_result1.last_run = djangotime.now()
|
||||
check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
|
||||
check_result1.save()
|
||||
if agent.plat == AgentPlat.WINDOWS:
|
||||
# disk space check
|
||||
check1 = Check()
|
||||
check1.agent = agent
|
||||
check1.check_type = CheckType.DISK_SPACE
|
||||
check1.warning_threshold = 25
|
||||
check1.error_threshold = 10
|
||||
check1.disk = "C:"
|
||||
check1.email_alert = random.choice([True, False])
|
||||
check1.text_alert = random.choice([True, False])
|
||||
check1.save()
|
||||
|
||||
check1.warning_threshold = 25
|
||||
check1.error_threshold = 10
|
||||
check1.disk = "C:"
|
||||
check1.email_alert = random.choice([True, False])
|
||||
check1.text_alert = random.choice([True, False])
|
||||
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):
|
||||
check1_history = CheckHistory()
|
||||
check1_history.check_id = check1.pk
|
||||
check1_history.agent_id = agent.agent_id
|
||||
check1_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check1_history.y = random.randint(13, 40)
|
||||
check1_history.save()
|
||||
for i in range(30):
|
||||
check1_history = CheckHistory()
|
||||
check1_history.check_id = check1.pk
|
||||
check1_history.agent_id = agent.agent_id
|
||||
check1_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check1_history.y = random.randint(13, 40)
|
||||
check1_history.save()
|
||||
|
||||
# ping check
|
||||
check2 = Check()
|
||||
check_result2 = CheckResult(assigned_check=check2, agent=agent)
|
||||
check_result2 = CheckResult()
|
||||
|
||||
check2.agent = agent
|
||||
check2.check_type = "ping"
|
||||
check_result2.last_run = djangotime.now()
|
||||
check2.check_type = CheckType.PING
|
||||
|
||||
check2.email_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:
|
||||
check2.name = "Synology NAS"
|
||||
check_result2.status = "failing"
|
||||
check2.alert_severity = AlertSeverity.ERROR
|
||||
check_result2.status = CheckStatus.FAILING
|
||||
check2.ip = "172.17.14.26"
|
||||
check_result2.more_info = ping_fail_output
|
||||
else:
|
||||
check2.name = "Google"
|
||||
check_result2.status = "passing"
|
||||
check_result2.status = CheckStatus.PASSING
|
||||
check2.ip = "8.8.8.8"
|
||||
check_result2.more_info = ping_success_output
|
||||
|
||||
@@ -409,9 +461,7 @@ class Command(BaseCommand):
|
||||
check2_history = CheckHistory()
|
||||
check2_history.check_id = check2.pk
|
||||
check2_history.agent_id = agent.agent_id
|
||||
check2_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check2_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
if site in sites5:
|
||||
check2_history.y = 1
|
||||
check2_history.results = ping_fail_output
|
||||
@@ -422,13 +472,19 @@ class Command(BaseCommand):
|
||||
|
||||
# cpu load check
|
||||
check3 = Check()
|
||||
check_result3 = CheckResult(assigned_check=check3, agent=agent)
|
||||
check3.agent = agent
|
||||
check3.check_type = "cpuload"
|
||||
check_result3.status = "passing"
|
||||
check_result3.last_run = djangotime.now()
|
||||
check3.check_type = CheckType.CPU_LOAD
|
||||
check3.warning_threshold = 70
|
||||
check3.error_threshold = 90
|
||||
check3.email_alert = random.choice([True, False])
|
||||
check3.text_alert = random.choice([True, False])
|
||||
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,
|
||||
@@ -445,68 +501,69 @@ class Command(BaseCommand):
|
||||
13,
|
||||
34,
|
||||
]
|
||||
check3.email_alert = random.choice([True, False])
|
||||
check3.text_alert = random.choice([True, False])
|
||||
check3.save()
|
||||
check_result3.save()
|
||||
|
||||
for i in range(30):
|
||||
check3_history = CheckHistory()
|
||||
check3_history.check_id = check3.pk
|
||||
check3_history.agent_id = agent.agent_id
|
||||
check3_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check3_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check3_history.y = random.randint(2, 79)
|
||||
check3_history.save()
|
||||
|
||||
# memory check
|
||||
check4 = Check()
|
||||
check_result4 = CheckResult(assigned_check=check4, agent=agent)
|
||||
check4.agent = agent
|
||||
check4.check_type = "memory"
|
||||
check_result4.status = "passing"
|
||||
check4.check_type = CheckType.MEMORY
|
||||
check4.warning_threshold = 70
|
||||
check4.error_threshold = 85
|
||||
check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
|
||||
check4.email_alert = random.choice([True, False])
|
||||
check4.text_alert = random.choice([True, False])
|
||||
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):
|
||||
check4_history = CheckHistory()
|
||||
check4_history.check_id = check4.pk
|
||||
check4_history.agent_id = agent.agent_id
|
||||
check4_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check4_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check4_history.y = random.randint(2, 79)
|
||||
check4_history.save()
|
||||
|
||||
# script check storage pool
|
||||
check5 = Check()
|
||||
check_result5 = CheckResult(assigned_check=check5, agent=agent)
|
||||
|
||||
check5.agent = agent
|
||||
check5.check_type = "script"
|
||||
check_result5.status = "passing"
|
||||
check_result5.last_run = djangotime.now()
|
||||
check5.check_type = CheckType.SCRIPT
|
||||
|
||||
check5.email_alert = random.choice([True, False])
|
||||
check5.text_alert = random.choice([True, False])
|
||||
check5.timeout = 120
|
||||
check_result5.retcode = 0
|
||||
check_result5.execution_time = "4.0000"
|
||||
|
||||
check5.script = check_pool_health
|
||||
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):
|
||||
check5_history = CheckHistory()
|
||||
check5_history.check_id = check5.pk
|
||||
check5_history.agent_id = agent.agent_id
|
||||
check5_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check5_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
if i == 10 or i == 18:
|
||||
check5_history.y = 1
|
||||
else:
|
||||
@@ -514,32 +571,34 @@ class Command(BaseCommand):
|
||||
check5_history.save()
|
||||
|
||||
check6 = Check()
|
||||
check_result6 = CheckResult(assigned_check=check6, agent=agent)
|
||||
|
||||
check6.agent = agent
|
||||
check6.check_type = "script"
|
||||
check_result6.status = "passing"
|
||||
check_result6.last_run = djangotime.now()
|
||||
check6.check_type = CheckType.SCRIPT
|
||||
check6.email_alert = random.choice([True, False])
|
||||
check6.text_alert = random.choice([True, False])
|
||||
check6.timeout = 120
|
||||
check_result6.retcode = 0
|
||||
check_result6.execution_time = "4.0000"
|
||||
check6.script = check_net_aware
|
||||
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):
|
||||
check6_history = CheckHistory()
|
||||
check6_history.check_id = check6.pk
|
||||
check6_history.agent_id = agent.agent_id
|
||||
check6_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check6_history.y = 0
|
||||
check6_history.save()
|
||||
|
||||
nla_task = AutomatedTask()
|
||||
nla_task_result = TaskResult(task=nla_task, agent=agent)
|
||||
|
||||
nla_task.agent = agent
|
||||
actions = [
|
||||
{
|
||||
@@ -553,17 +612,21 @@ class Command(BaseCommand):
|
||||
nla_task.actions = actions
|
||||
nla_task.assigned_check = check6
|
||||
nla_task.name = "Restart NLA"
|
||||
nla_task.task_type = "checkfailure"
|
||||
nla_task.task_type = TaskType.CHECK_FAILURE
|
||||
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 = djangotime.now()
|
||||
nla_task_result.last_run = django_now
|
||||
nla_task_result.stdout = "no stdout"
|
||||
nla_task_result.retcode = 0
|
||||
nla_task_result.sync_status = "synced"
|
||||
nla_task.save()
|
||||
nla_task_result.sync_status = TaskSyncStatus.SYNCED
|
||||
nla_task_result.save()
|
||||
|
||||
spool_task = AutomatedTask()
|
||||
spool_task_result = TaskResult(task=spool_task, agent=agent)
|
||||
|
||||
spool_task.agent = agent
|
||||
actions = [
|
||||
{
|
||||
@@ -576,25 +639,26 @@ class Command(BaseCommand):
|
||||
]
|
||||
spool_task.actions = actions
|
||||
spool_task.name = "Clear the print spooler"
|
||||
spool_task.task_type = "daily"
|
||||
spool_task.run_time_date = djangotime.now() + djangotime.timedelta(
|
||||
minutes=10
|
||||
)
|
||||
spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753)
|
||||
spool_task.task_type = TaskType.DAILY
|
||||
spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10)
|
||||
spool_task.expire_date = django_now + djangotime.timedelta(days=753)
|
||||
spool_task.daily_interval = 1
|
||||
spool_task.weekly_interval = 1
|
||||
spool_task.task_repetition_duration = "2h"
|
||||
spool_task.task_repetition_interval = "25m"
|
||||
spool_task.random_task_delay = "3m"
|
||||
spool_task_result.last_run = djangotime.now()
|
||||
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 = "synced"
|
||||
spool_task.save()
|
||||
spool_task_result.sync_status = TaskSyncStatus.SYNCED
|
||||
spool_task_result.save()
|
||||
|
||||
tmp_dir_task = AutomatedTask()
|
||||
tmp_dir_task_result = TaskResult(task=tmp_dir_task, agent=agent)
|
||||
tmp_dir_task.agent = agent
|
||||
tmp_dir_task.name = "show temp dir files"
|
||||
actions = [
|
||||
@@ -607,139 +671,148 @@ class Command(BaseCommand):
|
||||
}
|
||||
]
|
||||
tmp_dir_task.actions = actions
|
||||
tmp_dir_task.task_type = "manual"
|
||||
tmp_dir_task_result.last_run = djangotime.now()
|
||||
tmp_dir_task.task_type = TaskType.MANUAL
|
||||
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 = "synced"
|
||||
tmp_dir_task.save()
|
||||
tmp_dir_task_result.sync_status = TaskSyncStatus.SYNCED
|
||||
tmp_dir_task_result.save()
|
||||
|
||||
check7 = Check()
|
||||
check_result7 = CheckResult(assigned_check=check7, agent=agent)
|
||||
|
||||
check7.agent = agent
|
||||
check7.check_type = "script"
|
||||
check_result7.status = "passing"
|
||||
check_result7.last_run = djangotime.now()
|
||||
check7.check_type = CheckType.SCRIPT
|
||||
|
||||
check7.email_alert = random.choice([True, False])
|
||||
check7.text_alert = random.choice([True, False])
|
||||
check7.timeout = 120
|
||||
|
||||
check7.script = clear_spool
|
||||
|
||||
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"
|
||||
check7.script = clear_spool
|
||||
check_result7.stdout = spooler_stdout
|
||||
check7.save()
|
||||
check_result7.save()
|
||||
|
||||
for i in range(30):
|
||||
check7_history = CheckHistory()
|
||||
check7_history.check_id = check7.pk
|
||||
check7_history.agent_id = agent.agent_id
|
||||
check7_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
check7_history.y = 0
|
||||
check7_history.save()
|
||||
|
||||
check8 = Check()
|
||||
check_result8 = CheckResult(assigned_check=check8, agent=agent)
|
||||
check8.agent = agent
|
||||
check8.check_type = "winsvc"
|
||||
check_result8.status = "passing"
|
||||
check_result8.last_run = djangotime.now()
|
||||
check8.email_alert = random.choice([True, False])
|
||||
check8.text_alert = random.choice([True, False])
|
||||
check_result8.more_info = "Status RUNNING"
|
||||
check8.fails_b4_alert = 4
|
||||
check8.svc_name = "Spooler"
|
||||
check8.svc_display_name = "Print Spooler"
|
||||
check8.pass_if_start_pending = False
|
||||
check8.restart_if_stopped = True
|
||||
check8.save()
|
||||
check_result8.save()
|
||||
if agent.plat == AgentPlat.WINDOWS:
|
||||
check8 = Check()
|
||||
check8.agent = agent
|
||||
check8.check_type = CheckType.WINSVC
|
||||
check8.email_alert = random.choice([True, False])
|
||||
check8.text_alert = random.choice([True, False])
|
||||
check8.fails_b4_alert = 4
|
||||
check8.svc_name = "Spooler"
|
||||
check8.svc_display_name = "Print Spooler"
|
||||
check8.pass_if_start_pending = False
|
||||
check8.restart_if_stopped = True
|
||||
check8.save()
|
||||
|
||||
for i in range(30):
|
||||
check8_history = CheckHistory()
|
||||
check8_history.check_id = check8.pk
|
||||
check8_history.agent_id = agent.agent_id
|
||||
check8_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
if i == 10 or i == 18:
|
||||
check8_history.y = 1
|
||||
check8_history.results = "Status STOPPED"
|
||||
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):
|
||||
check8_history = CheckHistory()
|
||||
check8_history.check_id = check8.pk
|
||||
check8_history.agent_id = agent.agent_id
|
||||
check8_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
if i == 10 or i == 18:
|
||||
check8_history.y = 1
|
||||
check8_history.results = "Status STOPPED"
|
||||
else:
|
||||
check8_history.y = 0
|
||||
check8_history.results = "Status RUNNING"
|
||||
check8_history.save()
|
||||
|
||||
check9 = Check()
|
||||
check9.agent = agent
|
||||
check9.check_type = CheckType.EVENT_LOG
|
||||
check9.name = "unexpected shutdown"
|
||||
check9.email_alert = random.choice([True, False])
|
||||
check9.text_alert = random.choice([True, False])
|
||||
check9.fails_b4_alert = 2
|
||||
check9.log_name = EvtLogNames.APPLICATION
|
||||
check9.event_id = 1001
|
||||
check9.event_type = EvtLogTypes.INFO
|
||||
check9.fail_when = EvtLogFailWhen.CONTAINS
|
||||
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:
|
||||
check8_history.y = 0
|
||||
check8_history.results = "Status RUNNING"
|
||||
check8_history.save()
|
||||
check_result9.extra_details = {"log": []}
|
||||
check_result9.status = CheckStatus.PASSING
|
||||
|
||||
check9 = Check()
|
||||
check_result9 = CheckResult(assigned_check=check9, agent=agent)
|
||||
check9.agent = agent
|
||||
check9.check_type = "eventlog"
|
||||
check9.name = "unexpected shutdown"
|
||||
check9.save()
|
||||
check_result9.save()
|
||||
|
||||
check_result9.last_run = djangotime.now()
|
||||
check9.email_alert = random.choice([True, False])
|
||||
check9.text_alert = random.choice([True, False])
|
||||
check9.fails_b4_alert = 2
|
||||
for i in range(30):
|
||||
check9_history = CheckHistory()
|
||||
check9_history.check_id = check9.pk
|
||||
check9_history.agent_id = agent.agent_id
|
||||
check9_history.x = django_now - djangotime.timedelta(minutes=i * 2)
|
||||
if i == 10 or i == 18:
|
||||
check9_history.y = 1
|
||||
check9_history.results = "Events Found: 16"
|
||||
else:
|
||||
check9_history.y = 0
|
||||
check9_history.results = "Events Found: 0"
|
||||
check9_history.save()
|
||||
|
||||
if site in sites5:
|
||||
check_result9.extra_details = eventlog_check_fail_data
|
||||
check_result9.status = "failing"
|
||||
else:
|
||||
check_result9.extra_details = {"log": []}
|
||||
check_result9.status = "passing"
|
||||
pick = random.randint(1, 10)
|
||||
|
||||
check9.log_name = "Application"
|
||||
check9.event_id = 1001
|
||||
check9.event_type = "INFO"
|
||||
check9.fail_when = "contains"
|
||||
check9.search_last_days = 30
|
||||
if pick == 5 or pick == 3:
|
||||
|
||||
check9.save()
|
||||
check_result9.save()
|
||||
reboot_time = django_now + djangotime.timedelta(
|
||||
minutes=random.randint(1000, 500000)
|
||||
)
|
||||
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
|
||||
|
||||
for i in range(30):
|
||||
check9_history = CheckHistory()
|
||||
check9_history.check_id = check9.pk
|
||||
check9_history.agent_id = agent.agent_id
|
||||
check9_history.x = djangotime.now() - djangotime.timedelta(
|
||||
minutes=i * 2
|
||||
)
|
||||
if i == 10 or i == 18:
|
||||
check9_history.y = 1
|
||||
check9_history.results = "Events Found: 16"
|
||||
else:
|
||||
check9_history.y = 0
|
||||
check9_history.results = "Events Found: 0"
|
||||
check9_history.save()
|
||||
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
|
||||
|
||||
pick = random.randint(1, 10)
|
||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
||||
random.choice(string.ascii_letters) for _ in range(10)
|
||||
)
|
||||
|
||||
if pick == 5 or pick == 3:
|
||||
|
||||
reboot_time = djangotime.now() + djangotime.timedelta(
|
||||
minutes=random.randint(1000, 500000)
|
||||
)
|
||||
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
|
||||
|
||||
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
|
||||
|
||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
||||
random.choice(string.ascii_letters) for _ in range(10)
|
||||
)
|
||||
|
||||
sched_reboot = PendingAction()
|
||||
sched_reboot.agent = agent
|
||||
sched_reboot.action_type = "schedreboot"
|
||||
sched_reboot.details = {
|
||||
"time": str(obj),
|
||||
"taskname": task_name,
|
||||
}
|
||||
sched_reboot.save()
|
||||
sched_reboot = PendingAction()
|
||||
sched_reboot.agent = agent
|
||||
sched_reboot.action_type = PAAction.SCHED_REBOOT
|
||||
sched_reboot.details = {
|
||||
"time": str(obj),
|
||||
"taskname": task_name,
|
||||
}
|
||||
sched_reboot.save()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}"))
|
||||
|
||||
call_command("load_demo_scripts")
|
||||
self.stdout.write("done")
|
||||
|
||||
30
api/tacticalrmm/agents/management/commands/find_services.py
Normal file
30
api/tacticalrmm/agents/management/commands/find_services.py
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -1,16 +1,17 @@
|
||||
from agents.models import Agent
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ONLINE_AGENTS
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Shows online agents that are not on the latest version"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
|
||||
"pk", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
agents = [i for i in q if i.status == "online"]
|
||||
only = ONLINE_AGENTS + ("hostname",)
|
||||
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(*only)
|
||||
agents = [i for i in q if i.status == AGENT_STATUS_ONLINE]
|
||||
for agent in agents:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from agents.models import Agent
|
||||
from agents.tasks import send_agent_update_task
|
||||
from core.utils import get_core_settings
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from packaging import version as pyver
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.tasks import send_agent_update_task
|
||||
from core.utils import get_core_settings, token_is_valid
|
||||
from tacticalrmm.constants import AGENT_DEFER
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ class Command(BaseCommand):
|
||||
for i in q
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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',
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py
Normal file
18
api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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',
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py
Normal file
18
api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -2,29 +2,47 @@ import asyncio
|
||||
import re
|
||||
from collections import Counter
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Any, Optional, List, Dict, Union, Sequence, cast, TYPE_CHECKING
|
||||
from django.core.cache import cache
|
||||
from packaging import version as pyver
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
|
||||
|
||||
import msgpack
|
||||
import nats
|
||||
import validators
|
||||
from asgiref.sync import sync_to_async
|
||||
from core.models import TZ_CHOICES
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import BaseAuditModel, DebugLog
|
||||
from nats.errors import TimeoutError
|
||||
from packaging import version as pyver
|
||||
|
||||
from core.utils import get_core_settings
|
||||
from agents.utils import get_agent_url
|
||||
from core.models import TZ_CHOICES
|
||||
from core.utils import get_core_settings, send_command_with_mesh
|
||||
from logs.models import BaseAuditModel, DebugLog, PendingAction
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_STATUS_OFFLINE,
|
||||
AGENT_STATUS_ONLINE,
|
||||
AGENT_STATUS_OVERDUE,
|
||||
ONLINE_AGENTS,
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
AgentPlat,
|
||||
AlertSeverity,
|
||||
CheckStatus,
|
||||
CheckType,
|
||||
CustomFieldType,
|
||||
DebugLogType,
|
||||
GoArch,
|
||||
PAAction,
|
||||
PAStatus,
|
||||
)
|
||||
from tacticalrmm.helpers import get_nats_ports
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
from tacticalrmm.constants import ONLINE_AGENTS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from alerts.models import Alert, AlertTemplate
|
||||
from automation.models import Policy
|
||||
from alerts.models import AlertTemplate, Alert
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from clients.models import Client
|
||||
@@ -44,9 +62,12 @@ class Agent(BaseAuditModel):
|
||||
|
||||
version = models.CharField(default="0.1.0", max_length=255)
|
||||
operating_system = models.CharField(null=True, blank=True, max_length=255)
|
||||
plat = models.CharField(max_length=255, default="windows")
|
||||
goarch = models.CharField(max_length=255, null=True, blank=True)
|
||||
plat_release = models.CharField(max_length=255, null=True, blank=True)
|
||||
plat: "AgentPlat" = models.CharField( # type: ignore
|
||||
max_length=255, choices=AgentPlat.choices, default=AgentPlat.WINDOWS
|
||||
)
|
||||
goarch: "GoArch" = models.CharField( # type: ignore
|
||||
max_length=255, choices=GoArch.choices, null=True, blank=True
|
||||
)
|
||||
hostname = models.CharField(max_length=255)
|
||||
agent_id = models.CharField(max_length=200, unique=True)
|
||||
last_seen = models.DateTimeField(null=True, blank=True)
|
||||
@@ -57,7 +78,9 @@ class Agent(BaseAuditModel):
|
||||
boot_time = models.FloatField(null=True, blank=True)
|
||||
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
|
||||
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
|
||||
monitoring_type = models.CharField(max_length=30)
|
||||
monitoring_type = models.CharField(
|
||||
max_length=30, choices=AgentMonType.choices, default=AgentMonType.SERVER
|
||||
)
|
||||
description = models.CharField(null=True, blank=True, max_length=255)
|
||||
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
|
||||
overdue_email_alert = models.BooleanField(default=False)
|
||||
@@ -112,8 +135,9 @@ class Agent(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def is_posix(self) -> bool:
|
||||
return self.plat == "linux" or self.plat == "darwin"
|
||||
return self.plat in {AgentPlat.LINUX, AgentPlat.DARWIN}
|
||||
|
||||
# DEPRECATED, use goarch instead
|
||||
@property
|
||||
def arch(self) -> Optional[str]:
|
||||
if self.is_posix:
|
||||
@@ -126,21 +150,51 @@ class Agent(BaseAuditModel):
|
||||
return "32"
|
||||
return None
|
||||
|
||||
@property
|
||||
def winagent_dl(self) -> Optional[str]:
|
||||
if self.arch == "64":
|
||||
return settings.DL_64
|
||||
elif self.arch == "32":
|
||||
return settings.DL_32
|
||||
return None
|
||||
def do_update(self, *, token: str = "", force: bool = False) -> str:
|
||||
ver = settings.LATEST_AGENT_VER
|
||||
|
||||
@property
|
||||
def win_inno_exe(self) -> Optional[str]:
|
||||
if self.arch == "64":
|
||||
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
elif self.arch == "32":
|
||||
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
||||
return None
|
||||
if not self.goarch:
|
||||
DebugLog.warning(
|
||||
agent=self,
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Unable to determine arch on {self.hostname}({self.agent_id}). Skipping agent update.",
|
||||
)
|
||||
return "noarch"
|
||||
|
||||
if pyver.parse(self.version) <= pyver.parse("1.3.0"):
|
||||
return "not supported"
|
||||
|
||||
url = get_agent_url(goarch=self.goarch, plat=self.plat, token=token)
|
||||
bin = f"tacticalagent-v{ver}-{self.plat}-{self.goarch}.exe"
|
||||
|
||||
if not force:
|
||||
if self.pendingactions.filter( # type: ignore
|
||||
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
|
||||
).exists():
|
||||
self.pendingactions.filter( # type: ignore
|
||||
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
|
||||
).delete()
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=self,
|
||||
action_type=PAAction.AGENT_UPDATE,
|
||||
details={
|
||||
"url": url,
|
||||
"version": ver,
|
||||
"inno": bin,
|
||||
},
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": url,
|
||||
"version": ver,
|
||||
"inno": bin,
|
||||
},
|
||||
}
|
||||
asyncio.run(self.nats_cmd(nats_data, wait=False))
|
||||
return "created"
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
@@ -149,13 +203,13 @@ class Agent(BaseAuditModel):
|
||||
|
||||
if self.last_seen is not None:
|
||||
if (self.last_seen < offline) and (self.last_seen > overdue):
|
||||
return "offline"
|
||||
return AGENT_STATUS_OFFLINE
|
||||
elif (self.last_seen < offline) and (self.last_seen < overdue):
|
||||
return "overdue"
|
||||
return AGENT_STATUS_OVERDUE
|
||||
else:
|
||||
return "online"
|
||||
return AGENT_STATUS_ONLINE
|
||||
else:
|
||||
return "offline"
|
||||
return AGENT_STATUS_OFFLINE
|
||||
|
||||
@property
|
||||
def checks(self) -> Dict[str, Any]:
|
||||
@@ -168,23 +222,29 @@ class Agent(BaseAuditModel):
|
||||
if (
|
||||
not hasattr(check.check_result, "status")
|
||||
or isinstance(check.check_result, CheckResult)
|
||||
and check.check_result.status == "passing"
|
||||
and check.check_result.status == CheckStatus.PASSING
|
||||
):
|
||||
passing += 1
|
||||
elif (
|
||||
isinstance(check.check_result, CheckResult)
|
||||
and check.check_result.status == "failing"
|
||||
and check.check_result.status == CheckStatus.FAILING
|
||||
):
|
||||
alert_severity = (
|
||||
check.check_result.alert_severity
|
||||
if check.check_type in ["memory", "cpuload", "diskspace", "script"]
|
||||
if check.check_type
|
||||
in [
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
else check.alert_severity
|
||||
)
|
||||
if alert_severity == "error":
|
||||
if alert_severity == AlertSeverity.ERROR:
|
||||
failing += 1
|
||||
elif alert_severity == "warning":
|
||||
elif alert_severity == AlertSeverity.WARNING:
|
||||
warning += 1
|
||||
elif alert_severity == "info":
|
||||
elif alert_severity == AlertSeverity.INFO:
|
||||
info += 1
|
||||
|
||||
ret = {
|
||||
@@ -348,10 +408,14 @@ class Agent(BaseAuditModel):
|
||||
i
|
||||
for i in cls.objects.only(*ONLINE_AGENTS)
|
||||
if pyver.parse(i.version) >= pyver.parse(min_version)
|
||||
and i.status == "online"
|
||||
and i.status == AGENT_STATUS_ONLINE
|
||||
]
|
||||
|
||||
return [i for i in cls.objects.only(*ONLINE_AGENTS) if i.status == "online"]
|
||||
return [
|
||||
i
|
||||
for i in cls.objects.only(*ONLINE_AGENTS)
|
||||
if i.status == AGENT_STATUS_ONLINE
|
||||
]
|
||||
|
||||
def is_supported_script(self, platforms: List[str]) -> bool:
|
||||
return self.plat.lower() in platforms if platforms else True
|
||||
@@ -718,13 +782,14 @@ class Agent(BaseAuditModel):
|
||||
return tasks
|
||||
|
||||
def _do_nats_debug(self, agent: "Agent", message: str) -> None:
|
||||
DebugLog.error(agent=agent, log_type="agent_issues", message=message)
|
||||
DebugLog.error(agent=agent, log_type=DebugLogType.AGENT_ISSUES, message=message)
|
||||
|
||||
async def nats_cmd(
|
||||
self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
|
||||
) -> Any:
|
||||
nats_std_port, _ = get_nats_ports()
|
||||
options = {
|
||||
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
|
||||
"user": "tacticalrmm",
|
||||
"password": settings.SECRET_KEY,
|
||||
"connect_timeout": 3,
|
||||
@@ -759,6 +824,38 @@ class Agent(BaseAuditModel):
|
||||
await nc.flush()
|
||||
await nc.close()
|
||||
|
||||
def recover(self, mode: str, mesh_uri: str, wait: bool = True) -> tuple[str, bool]:
|
||||
"""
|
||||
Return type: tuple(message: str, error: bool)
|
||||
"""
|
||||
if mode == "tacagent":
|
||||
if self.is_posix:
|
||||
cmd = "systemctl restart tacticalagent.service"
|
||||
shell = 3
|
||||
else:
|
||||
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
|
||||
shell = 1
|
||||
|
||||
asyncio.run(
|
||||
send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
|
||||
)
|
||||
return ("ok", False)
|
||||
|
||||
elif mode == "mesh":
|
||||
data = {"func": "recover", "payload": {"mode": mode}}
|
||||
if wait:
|
||||
r = asyncio.run(self.nats_cmd(data, timeout=20))
|
||||
if r == "ok":
|
||||
return ("ok", False)
|
||||
else:
|
||||
return (str(r), True)
|
||||
else:
|
||||
asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
|
||||
|
||||
return ("ok", False)
|
||||
|
||||
return ("invalid", True)
|
||||
|
||||
@staticmethod
|
||||
def serialize(agent: "Agent") -> Dict[str, Any]:
|
||||
# serializes the agent and returns json
|
||||
@@ -908,39 +1005,30 @@ class AgentCustomField(models.Model):
|
||||
|
||||
@property
|
||||
def value(self) -> Union[List[Any], bool, str]:
|
||||
if self.field.type == "multiple":
|
||||
if self.field.type == CustomFieldType.MULTIPLE:
|
||||
return cast(List[str], self.multiple_value)
|
||||
elif self.field.type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return cast(str, self.string_value)
|
||||
|
||||
def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
|
||||
if self.field.type in [
|
||||
"text",
|
||||
"number",
|
||||
"single",
|
||||
"datetime",
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
self.string_value = cast(str, value)
|
||||
self.save()
|
||||
elif self.field.type == "multiple":
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
self.multiple_value = value.split(",")
|
||||
self.save()
|
||||
elif self.field.type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
self.bool_value = bool(value)
|
||||
self.save()
|
||||
|
||||
|
||||
AGENT_HISTORY_TYPES = (
|
||||
("task_run", "Task Run"),
|
||||
("script_run", "Script Run"),
|
||||
("cmd_run", "CMD Run"),
|
||||
)
|
||||
|
||||
AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
|
||||
|
||||
|
||||
class AgentHistory(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
@@ -950,13 +1038,12 @@ class AgentHistory(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
type = models.CharField(
|
||||
max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run"
|
||||
type: "AgentHistoryType" = models.CharField(
|
||||
max_length=50,
|
||||
choices=AgentHistoryType.choices,
|
||||
default=AgentHistoryType.CMD_RUN,
|
||||
)
|
||||
command = models.TextField(null=True, blank=True, default="")
|
||||
status = models.CharField(
|
||||
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
|
||||
)
|
||||
username = models.CharField(max_length=255, default="system")
|
||||
results = models.TextField(null=True, blank=True)
|
||||
script = models.ForeignKey(
|
||||
|
||||
@@ -27,6 +27,9 @@ class AgentPerms(permissions.BasePermission):
|
||||
|
||||
class RecoverAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view) -> bool:
|
||||
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(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytz
|
||||
from rest_framework import serializers
|
||||
|
||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Agent, AgentCustomField, AgentHistory, Note
|
||||
@@ -102,7 +104,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
|
||||
def get_logged_username(self, obj) -> str:
|
||||
if obj.logged_in_username == "None" and obj.status == "online":
|
||||
if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE:
|
||||
return obj.last_logged_in_user
|
||||
elif obj.logged_in_username != "None":
|
||||
return obj.logged_in_username
|
||||
@@ -110,7 +112,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
return "-"
|
||||
|
||||
def get_italic(self, obj) -> bool:
|
||||
return obj.logged_in_username == "None" and obj.status == "online"
|
||||
return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
|
||||
@@ -1,107 +1,39 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.utils import get_agent_url
|
||||
from core.utils import get_core_settings
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import DebugLog, PendingAction
|
||||
from packaging import version as pyver
|
||||
from logs.models import DebugLog
|
||||
from scripts.models import Script
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_DEFER,
|
||||
AGENT_STATUS_OVERDUE,
|
||||
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_agent_url(agent.arch, agent.plat)
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
}
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
return "created"
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
@app.task
|
||||
def force_code_sign(agent_ids: list[str]) -> None:
|
||||
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
|
||||
for chunk in chunks:
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id=agent_id, force=True)
|
||||
sleep(2)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(agent_ids: list[str]) -> None:
|
||||
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
|
||||
for chunk in chunks:
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id)
|
||||
sleep(2)
|
||||
def send_agent_update_task(*, agent_ids: list[str], token: str, force: bool) -> None:
|
||||
agents: "QuerySet[Agent]" = Agent.objects.defer(*AGENT_DEFER).filter(
|
||||
agent_id__in=agent_ids
|
||||
)
|
||||
for agent in agents:
|
||||
agent.do_update(token=token, force=force)
|
||||
|
||||
|
||||
@app.task
|
||||
def auto_self_agent_update_task() -> None:
|
||||
core = get_core_settings()
|
||||
if not core.agent_auto_update:
|
||||
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 + 70] for i in range(0, len(agent_ids), 70))
|
||||
for chunk in chunks:
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id)
|
||||
sleep(2)
|
||||
call_command("update_agents")
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -209,7 +141,7 @@ def agent_outages_task() -> None:
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
if agent.status == "overdue":
|
||||
if agent.status == AGENT_STATUS_OVERDUE:
|
||||
Alert.handle_alert_failure(agent)
|
||||
|
||||
|
||||
@@ -235,7 +167,7 @@ def run_script_email_results_task(
|
||||
if r == "timeout":
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type="scripting",
|
||||
log_type=DebugLogType.SCRIPTING,
|
||||
message=f"{agent.hostname}({agent.pk}) timed out running script.",
|
||||
)
|
||||
return
|
||||
@@ -289,7 +221,7 @@ def clear_faults_task(older_than_days: int) -> None:
|
||||
for check in agent.get_checks_with_policies():
|
||||
# reset check status
|
||||
if check.check_result:
|
||||
check.check_result.status = "passing"
|
||||
check.check_result.status = CheckStatus.PASSING
|
||||
check.check_result.save(update_fields=["status"])
|
||||
if check.alert.filter(agent=agent, resolved=False).exists():
|
||||
alert = Alert.create_or_return_check_alert(check, agent=agent)
|
||||
@@ -318,3 +250,8 @@ def prune_agent_history(older_than_days: int) -> str:
|
||||
).delete()
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def bulk_recover_agents_task() -> None:
|
||||
call_command("bulk_restart_agents")
|
||||
|
||||
0
api/tacticalrmm/agents/tests/__init__.py
Normal file
0
api/tacticalrmm/agents/tests/__init__.py
Normal file
106
api/tacticalrmm/agents/tests/test_agent_installs.py
Normal file
106
api/tacticalrmm/agents/tests/test_agent_installs.py
Normal file
@@ -0,0 +1,106 @@
|
||||
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)
|
||||
313
api/tacticalrmm/agents/tests/test_agent_update.py
Normal file
313
api/tacticalrmm/agents/tests/test_agent_update.py
Normal file
@@ -0,0 +1,313 @@
|
||||
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]
|
||||
# )
|
||||
60
api/tacticalrmm/agents/tests/test_agent_utils.py
Normal file
60
api/tacticalrmm/agents/tests/test_agent_utils.py
Normal file
@@ -0,0 +1,60 @@
|
||||
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)
|
||||
@@ -1,28 +1,32 @@
|
||||
import json
|
||||
import os
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
import pytz
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.test import modify_settings
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import PendingAction
|
||||
from model_bakery import baker
|
||||
from packaging import version as pyver
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import Agent, AgentCustomField, AgentHistory, Note
|
||||
from .serializers import (
|
||||
from agents.models import Agent, AgentCustomField, AgentHistory, Note
|
||||
from agents.serializers import (
|
||||
AgentHistorySerializer,
|
||||
AgentHostnameSerializer,
|
||||
AgentNoteSerializer,
|
||||
AgentSerializer,
|
||||
)
|
||||
from .tasks import auto_self_agent_update_task
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_STATUS_OFFLINE,
|
||||
AGENT_STATUS_ONLINE,
|
||||
AgentMonType,
|
||||
CustomFieldModel,
|
||||
CustomFieldType,
|
||||
EvtLogNames,
|
||||
)
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clients.models import Client, Site
|
||||
@@ -30,11 +34,6 @@ if TYPE_CHECKING:
|
||||
base_url = "/agents"
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestAgentsList(TacticalTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.authenticate()
|
||||
@@ -51,24 +50,27 @@ class TestAgentsList(TacticalTestCase):
|
||||
site3: "Site" = baker.make("clients.Site", client=company2)
|
||||
|
||||
baker.make_recipe(
|
||||
"agents.online_agent", site=site1, monitoring_type="server", _quantity=15
|
||||
"agents.online_agent",
|
||||
site=site1,
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
_quantity=15,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.online_agent",
|
||||
site=site2,
|
||||
monitoring_type="workstation",
|
||||
monitoring_type=AgentMonType.WORKSTATION,
|
||||
_quantity=10,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.online_agent",
|
||||
site=site3,
|
||||
monitoring_type="server",
|
||||
monitoring_type=AgentMonType.SERVER,
|
||||
_quantity=4,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.online_agent",
|
||||
site=site3,
|
||||
monitoring_type="workstation",
|
||||
monitoring_type=AgentMonType.WORKSTATION,
|
||||
_quantity=7,
|
||||
)
|
||||
|
||||
@@ -100,11 +102,6 @@ class TestAgentsList(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestAgentViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
@@ -117,6 +114,17 @@ class TestAgentViews(TacticalTestCase):
|
||||
)
|
||||
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
|
||||
|
||||
@patch("agents.tasks.bulk_recover_agents_task.delay")
|
||||
def test_bulk_agent_recovery(self, mock_task):
|
||||
mock_task.return_value = None
|
||||
url = f"{base_url}/bulkrecovery/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
mock_task.assert_called_once()
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_agent(self):
|
||||
url = f"{base_url}/{self.agent.agent_id}/"
|
||||
|
||||
@@ -170,7 +178,11 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.assertEqual(data["run_time_days"], [2, 3, 6])
|
||||
|
||||
# test adding custom fields
|
||||
field = baker.make("core.CustomField", model="agent", type="number")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
model=CustomFieldModel.AGENT,
|
||||
type=CustomFieldType.NUMBER,
|
||||
)
|
||||
data = {
|
||||
"site": site.pk,
|
||||
"description": "asjdk234andasd",
|
||||
@@ -227,7 +239,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.agent.save(update_fields=["policy"])
|
||||
_ = self.agent.get_patch_policy()
|
||||
|
||||
self.agent.monitoring_type = "workstation"
|
||||
self.agent.monitoring_type = AgentMonType.WORKSTATION
|
||||
self.agent.save(update_fields=["monitoring_type"])
|
||||
_ = self.agent.get_patch_policy()
|
||||
|
||||
@@ -239,52 +251,21 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.coresettings.save(update_fields=["server_policy", "workstation_policy"])
|
||||
_ = self.agent.get_patch_policy()
|
||||
|
||||
self.agent.monitoring_type = "server"
|
||||
self.agent.monitoring_type = AgentMonType.SERVER
|
||||
self.agent.save(update_fields=["monitoring_type"])
|
||||
_ = self.agent.get_patch_policy()
|
||||
|
||||
def test_get_agent_versions(self):
|
||||
url = "/agents/versions/"
|
||||
r = self.client.get(url)
|
||||
|
||||
with self.assertNumQueries(1):
|
||||
r = self.client.get(url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"])
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.tasks.send_agent_update_task.delay")
|
||||
def test_update_agents(self, mock_task):
|
||||
url = f"{base_url}/update/"
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
_quantity=15,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.3.0",
|
||||
_quantity=15,
|
||||
)
|
||||
|
||||
agent_ids: list[str] = list(
|
||||
Agent.objects.only("agent_id", "version").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_task.assert_called_with(agent_ids=expected)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("time.sleep", return_value=None)
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_agent_ping(self, nats_cmd, mock_sleep):
|
||||
@@ -293,25 +274,25 @@ class TestAgentViews(TacticalTestCase):
|
||||
nats_cmd.return_value = "timeout"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
ret = {"name": self.agent.hostname, "status": "offline"}
|
||||
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
|
||||
self.assertEqual(r.json(), ret)
|
||||
|
||||
nats_cmd.return_value = "natsdown"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
ret = {"name": self.agent.hostname, "status": "offline"}
|
||||
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
|
||||
self.assertEqual(r.json(), ret)
|
||||
|
||||
nats_cmd.return_value = "pong"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
ret = {"name": self.agent.hostname, "status": "online"}
|
||||
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_ONLINE}
|
||||
self.assertEqual(r.json(), ret)
|
||||
|
||||
nats_cmd.return_value = "asdasjdaksdasd"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
ret = {"name": self.agent.hostname, "status": "offline"}
|
||||
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
|
||||
self.assertEqual(r.json(), ret)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
@@ -371,7 +352,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
"func": "eventlog",
|
||||
"timeout": 30,
|
||||
"payload": {
|
||||
"logname": "Application",
|
||||
"logname": EvtLogNames.APPLICATION,
|
||||
"days": str(22),
|
||||
},
|
||||
},
|
||||
@@ -386,7 +367,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
"func": "eventlog",
|
||||
"timeout": 180,
|
||||
"payload": {
|
||||
"logname": "Security",
|
||||
"logname": EvtLogNames.SECURITY,
|
||||
"days": str(6),
|
||||
},
|
||||
},
|
||||
@@ -436,16 +417,20 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_reboot_later(self, nats_cmd):
|
||||
nats_cmd.return_value = "ok"
|
||||
url = f"{base_url}/{self.agent.agent_id}/reboot/"
|
||||
|
||||
data = {
|
||||
"datetime": "2025-08-29T18:41:02",
|
||||
}
|
||||
# ensure we don't allow dates in past
|
||||
data = {"datetime": "2022-07-11T01:51:11"}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "Date cannot be set in the past")
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
# test with date in future
|
||||
data["datetime"] = "2027-08-29T18:41:02"
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
|
||||
self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
|
||||
self.assertEqual(r.data["agent"], self.agent.hostname)
|
||||
|
||||
nats_data = {
|
||||
@@ -458,12 +443,12 @@ class TestAgentViews(TacticalTestCase):
|
||||
"multiple_instances": 2,
|
||||
"trigger": "runonce",
|
||||
"name": r.data["task_name"],
|
||||
"start_year": 2025,
|
||||
"start_year": 2027,
|
||||
"start_month": 8,
|
||||
"start_day": 29,
|
||||
"start_hour": 18,
|
||||
"start_min": 41,
|
||||
"expire_year": 2025,
|
||||
"expire_year": 2027,
|
||||
"expire_month": 8,
|
||||
"expire_day": 29,
|
||||
"expire_hour": 18,
|
||||
@@ -486,42 +471,6 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_install_agent(self):
|
||||
url = f"{base_url}/installer/"
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
data = {
|
||||
"client": site.client.pk,
|
||||
"site": site.pk,
|
||||
"arch": "64",
|
||||
"expires": 23,
|
||||
"installMethod": "manual",
|
||||
"api": "https://api.example.com",
|
||||
"agenttype": "server",
|
||||
"rdp": 1,
|
||||
"ping": 0,
|
||||
"power": 0,
|
||||
"fileName": "rmm-client-site-server.exe",
|
||||
}
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data["arch"] = "64"
|
||||
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"])
|
||||
|
||||
data["installMethod"] = "powershell"
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("meshctrl.utils.get_login_token")
|
||||
def test_meshcentral_tabs(self, mock_token):
|
||||
url = f"{base_url}/{self.agent.agent_id}/meshcentral/"
|
||||
@@ -576,10 +525,9 @@ class TestAgentViews(TacticalTestCase):
|
||||
@patch("agents.tasks.run_script_email_results_task.delay")
|
||||
@patch("agents.models.Agent.run_script")
|
||||
def test_run_script(self, run_script, email_task):
|
||||
from agents.models import AgentCustomField, AgentHistory, Note
|
||||
from clients.models import ClientCustomField, SiteCustomField
|
||||
|
||||
from .models import AgentCustomField, AgentHistory, Note
|
||||
|
||||
run_script.return_value = "ok"
|
||||
url = f"/agents/{self.agent.agent_id}/runscript/"
|
||||
script = baker.make_recipe("scripts.script")
|
||||
@@ -657,7 +605,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
# test collector
|
||||
|
||||
# save to agent custom field
|
||||
custom_field = baker.make("core.CustomField", model="agent")
|
||||
custom_field = baker.make("core.CustomField", model=CustomFieldModel.AGENT)
|
||||
data = {
|
||||
"script": script.pk,
|
||||
"output": "collector",
|
||||
@@ -721,7 +669,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
)
|
||||
|
||||
# save to client custom field
|
||||
custom_field = baker.make("core.CustomField", model="client")
|
||||
custom_field = baker.make("core.CustomField", model=CustomFieldModel.CLIENT)
|
||||
data = {
|
||||
"script": script.pk,
|
||||
"output": "collector",
|
||||
@@ -883,11 +831,6 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.assertEqual(r.data, data) # type:ignore
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestAgentViewsNew(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
@@ -922,11 +865,6 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestAgentPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_client()
|
||||
@@ -1122,55 +1060,6 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
self.check_authorized("post", url, site_data)
|
||||
self.check_authorized("post", url, client_data)
|
||||
|
||||
@patch("agents.tasks.send_agent_update_task.delay")
|
||||
def test_agent_update_permissions(self, update_task):
|
||||
agents = baker.make_recipe("agents.agent", _quantity=5)
|
||||
other_agents = baker.make_recipe("agents.agent", _quantity=7)
|
||||
|
||||
url = f"{base_url}/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"])
|
||||
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"])
|
||||
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]
|
||||
# )
|
||||
|
||||
def test_get_agent_version_permissions(self):
|
||||
agents = baker.make_recipe("agents.agent", _quantity=5)
|
||||
other_agents = baker.make_recipe("agents.agent", _quantity=7)
|
||||
@@ -1400,154 +1289,13 @@ class TestAgentPermissions(TacticalTestCase):
|
||||
self.check_authorized_superuser("get", unauthorized_url)
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestAgentTasks(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.utils.get_agent_url")
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_agent_update(self, nats_cmd, get_url):
|
||||
get_url.return_value = "https://exe.tacticalrmm.io"
|
||||
|
||||
from agents.tasks import agent_update
|
||||
|
||||
agent_noarch = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Error getting OS",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
)
|
||||
r = agent_update(agent_noarch.agent_id)
|
||||
self.assertEqual(r, "noarch")
|
||||
|
||||
agent_130 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.3.0",
|
||||
)
|
||||
r = agent_update(agent_130.agent_id)
|
||||
self.assertEqual(r, "not supported")
|
||||
|
||||
# test __without__ code signing
|
||||
agent64_nosign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
r = agent_update(agent64_nosign.agent_id)
|
||||
self.assertEqual(r, "created")
|
||||
action = PendingAction.objects.get(agent__agent_id=agent64_nosign.agent_id)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
self.assertEqual(
|
||||
action.details["url"],
|
||||
f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
)
|
||||
self.assertEqual(
|
||||
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
)
|
||||
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
|
||||
# test __with__ code signing (64 bit)
|
||||
""" codesign = baker.make("core.CodeSignToken", token="testtoken123")
|
||||
agent64_sign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
get_exe.return_value = "https://exe.tacticalrmm.io"
|
||||
r = agent_update(agent64_sign.pk, codesign.token)
|
||||
self.assertEqual(r, "created")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123",
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
# test __with__ code signing (32 bit)
|
||||
agent32_sign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
get_exe.return_value = "https://exe.tacticalrmm.io"
|
||||
r = agent_update(agent32_sign.pk, codesign.token)
|
||||
self.assertEqual(r, "created")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123",
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending") """
|
||||
|
||||
@patch("agents.tasks.agent_update")
|
||||
@patch("agents.tasks.sleep", return_value=None)
|
||||
def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
_quantity=23,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.3.0",
|
||||
_quantity=33,
|
||||
)
|
||||
|
||||
self.coresettings.agent_auto_update = False
|
||||
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||
|
||||
r = auto_self_agent_update_task.s().apply()
|
||||
self.assertEqual(agent_update.call_count, 0)
|
||||
|
||||
self.coresettings.agent_auto_update = True
|
||||
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||
|
||||
r = auto_self_agent_update_task.s().apply()
|
||||
self.assertEqual(agent_update.call_count, 33)
|
||||
|
||||
def test_agent_history_prune_task(self):
|
||||
from .tasks import prune_agent_history
|
||||
from agents.tasks import prune_agent_history
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
46
api/tacticalrmm/agents/tests/test_mgmt_commands.py
Normal file
46
api/tacticalrmm/agents/tests/test_mgmt_commands.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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)
|
||||
63
api/tacticalrmm/agents/tests/test_recovery.py
Normal file
63
api/tacticalrmm/agents/tests/test_recovery.py
Normal file
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from autotasks.views import GetAddAutoTasks
|
||||
from checks.views import GetAddChecks
|
||||
from django.urls import path
|
||||
from logs.views import PendingActions
|
||||
|
||||
from . import views
|
||||
@@ -40,4 +41,5 @@ urlpatterns = [
|
||||
path("versions/", views.get_agent_versions),
|
||||
path("update/", views.update_agents),
|
||||
path("installer/", views.install_agent),
|
||||
path("bulkrecovery/", views.bulk_agent_recovery),
|
||||
]
|
||||
|
||||
@@ -2,37 +2,26 @@ import asyncio
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
|
||||
from core.models import CodeSignToken
|
||||
from core.utils import get_mesh_device_id, get_mesh_ws_url, get_core_settings
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse
|
||||
|
||||
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
|
||||
from tacticalrmm.constants import MeshAgentIdent
|
||||
|
||||
|
||||
def get_agent_url(arch: str, plat: str) -> str:
|
||||
|
||||
if plat == "windows":
|
||||
endpoint = "winagents"
|
||||
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
|
||||
else:
|
||||
endpoint = "linuxagents"
|
||||
dl_url = ""
|
||||
|
||||
token = CodeSignToken.objects.first()
|
||||
if not token:
|
||||
return dl_url
|
||||
|
||||
if token.is_valid:
|
||||
base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?"
|
||||
def get_agent_url(*, goarch: str, plat: str, token: str = "") -> str:
|
||||
ver = settings.LATEST_AGENT_VER
|
||||
if token:
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
"token": token.token,
|
||||
"version": ver,
|
||||
"arch": goarch,
|
||||
"token": token,
|
||||
"plat": plat,
|
||||
"api": settings.ALLOWED_HOSTS[0],
|
||||
}
|
||||
dl_url = base_url + urllib.parse.urlencode(params)
|
||||
return settings.AGENTS_URL + urllib.parse.urlencode(params)
|
||||
|
||||
return dl_url
|
||||
return f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-{plat}-{goarch}.exe"
|
||||
|
||||
|
||||
def generate_linux_install(
|
||||
|
||||
@@ -4,40 +4,52 @@ import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from meshctrl.utils import get_login_token
|
||||
|
||||
from core.models import CodeSignToken
|
||||
from core.utils import (
|
||||
get_mesh_ws_url,
|
||||
remove_mesh_agent,
|
||||
send_command_with_mesh,
|
||||
get_core_settings,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.db.models import Q, Prefetch, Exists, Count, OuterRef
|
||||
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import AuditLog, DebugLog, PendingAction
|
||||
from meshctrl.utils import get_login_token
|
||||
from packaging import version as pyver
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from core.utils import (
|
||||
get_core_settings,
|
||||
get_mesh_ws_url,
|
||||
remove_mesh_agent,
|
||||
token_is_valid,
|
||||
)
|
||||
from logs.models import AuditLog, DebugLog, PendingAction
|
||||
from scripts.models import Script
|
||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||
from winupdate.models import WinUpdate
|
||||
|
||||
from tacticalrmm.constants import AGENT_DEFER
|
||||
from tacticalrmm.constants import (
|
||||
AGENT_DEFER,
|
||||
AGENT_STATUS_OFFLINE,
|
||||
AGENT_STATUS_ONLINE,
|
||||
AgentHistoryType,
|
||||
AgentMonType,
|
||||
AgentPlat,
|
||||
CustomFieldModel,
|
||||
DebugLogType,
|
||||
EvtLogNames,
|
||||
PAAction,
|
||||
PAStatus,
|
||||
)
|
||||
from tacticalrmm.helpers import date_is_in_past, notify_error
|
||||
from tacticalrmm.permissions import (
|
||||
_has_perm_on_agent,
|
||||
_has_perm_on_client,
|
||||
_has_perm_on_site,
|
||||
)
|
||||
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
|
||||
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 .permissions import (
|
||||
@@ -64,7 +76,11 @@ from .serializers import (
|
||||
AgentSerializer,
|
||||
AgentTableSerializer,
|
||||
)
|
||||
from .tasks import run_script_email_results_task, send_agent_update_task
|
||||
from .tasks import (
|
||||
bulk_recover_agents_task,
|
||||
run_script_email_results_task,
|
||||
send_agent_update_task,
|
||||
)
|
||||
|
||||
|
||||
class GetAgents(APIView):
|
||||
@@ -78,7 +94,7 @@ class GetAgents(APIView):
|
||||
|
||||
monitoring_type = request.query_params.get("monitoring_type", None)
|
||||
if monitoring_type:
|
||||
if monitoring_type in ["server", "workstation"]:
|
||||
if monitoring_type in AgentMonType.values:
|
||||
monitoring_type_filter = Q(monitoring_type=monitoring_type)
|
||||
else:
|
||||
return notify_error("monitoring type does not exist")
|
||||
@@ -119,7 +135,8 @@ class GetAgents(APIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_actions_count=Count(
|
||||
"pendingactions", filter=Q(pendingactions__status="pending")
|
||||
"pendingactions",
|
||||
filter=Q(pendingactions__status=PAStatus.PENDING),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -136,10 +153,10 @@ class GetAgents(APIView):
|
||||
else:
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user) # type: ignore
|
||||
.select_related("site")
|
||||
.defer(*AGENT_DEFER)
|
||||
.select_related("site__client")
|
||||
.filter(monitoring_type_filter)
|
||||
.filter(client_site_filter)
|
||||
.only("agent_id", "hostname", "site")
|
||||
)
|
||||
serializer = AgentHostnameSerializer(agents, many=True)
|
||||
|
||||
@@ -200,7 +217,7 @@ class GetUpdateDeleteAgent(APIView):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
code = "foo"
|
||||
if agent.plat == "linux":
|
||||
if agent.plat == AgentPlat.LINUX:
|
||||
with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
|
||||
code = f.read()
|
||||
|
||||
@@ -209,8 +226,14 @@ class GetUpdateDeleteAgent(APIView):
|
||||
mesh_id = agent.mesh_node_id
|
||||
agent.delete()
|
||||
reload_nats()
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(remove_mesh_agent(uri, mesh_id))
|
||||
try:
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(remove_mesh_agent(uri, mesh_id))
|
||||
except Exception as e:
|
||||
DebugLog.error(
|
||||
message=f"Unable to remove agent {name} from meshcentral database: {str(e)}",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
)
|
||||
return Response(f"{name} will now be uninstalled.")
|
||||
|
||||
|
||||
@@ -297,9 +320,9 @@ class AgentMeshCentral(APIView):
|
||||
@permission_classes([IsAuthenticated, AgentPerms])
|
||||
def get_agent_versions(request):
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user) # type: ignore
|
||||
.prefetch_related("site")
|
||||
.only("pk", "hostname")
|
||||
Agent.objects.defer(*AGENT_DEFER)
|
||||
.filter_by_role(request.user) # type: ignore
|
||||
.select_related("site__client")
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
@@ -322,7 +345,9 @@ def update_agents(request):
|
||||
for i in q
|
||||
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")
|
||||
|
||||
|
||||
@@ -330,12 +355,12 @@ def update_agents(request):
|
||||
@permission_classes([IsAuthenticated, PingAgentPerms])
|
||||
def ping(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
status = "offline"
|
||||
status = AGENT_STATUS_OFFLINE
|
||||
attempts = 0
|
||||
while 1:
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
status = AGENT_STATUS_ONLINE
|
||||
break
|
||||
else:
|
||||
attempts += 1
|
||||
@@ -356,7 +381,7 @@ def get_event_log(request, agent_id, logtype, days):
|
||||
return demo_get_eventlog()
|
||||
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
timeout = 180 if logtype == EvtLogNames.SECURITY else 30
|
||||
|
||||
data = {
|
||||
"func": "eventlog",
|
||||
@@ -394,7 +419,7 @@ def send_raw_cmd(request, agent_id):
|
||||
|
||||
hist = AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type="cmd_run",
|
||||
type=AgentHistoryType.CMD_RUN,
|
||||
command=request.data["cmd"],
|
||||
username=request.user.username[:50],
|
||||
)
|
||||
@@ -430,12 +455,17 @@ class Reboot(APIView):
|
||||
# reboot later
|
||||
def patch(self, request, 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:
|
||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S")
|
||||
except Exception:
|
||||
return notify_error("Invalid date")
|
||||
|
||||
if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone):
|
||||
return notify_error("Date cannot be set in the past")
|
||||
|
||||
task_name = "TacticalRMM_SchedReboot_" + "".join(
|
||||
random.choice(string.ascii_letters) for _ in range(10)
|
||||
)
|
||||
@@ -471,7 +501,7 @@ class Reboot(APIView):
|
||||
|
||||
details = {"taskname": task_name, "time": str(obj)}
|
||||
PendingAction.objects.create(
|
||||
agent=agent, action_type="schedreboot", details=details
|
||||
agent=agent, action_type=PAAction.SCHED_REBOOT, details=details
|
||||
)
|
||||
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
||||
return Response(
|
||||
@@ -482,27 +512,25 @@ class Reboot(APIView):
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
|
||||
def install_agent(request):
|
||||
from knox.models import AuthToken
|
||||
|
||||
from accounts.models import User
|
||||
from agents.utils import get_agent_url
|
||||
from knox.models import AuthToken
|
||||
from core.utils import token_is_valid
|
||||
|
||||
client_id = request.data["client"]
|
||||
site_id = request.data["site"]
|
||||
version = settings.LATEST_AGENT_VER
|
||||
arch = request.data["arch"]
|
||||
goarch = request.data["goarch"]
|
||||
plat = request.data["plat"]
|
||||
|
||||
if not _has_perm_on_site(request.user, site_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
inno = (
|
||||
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
|
||||
)
|
||||
if request.data["installMethod"] == "linux":
|
||||
plat = "linux"
|
||||
else:
|
||||
plat = "windows"
|
||||
codesign_token, is_valid = token_is_valid()
|
||||
|
||||
download_url = get_agent_url(arch, plat)
|
||||
inno = f"tacticalagent-v{version}-{plat}-{goarch}.exe"
|
||||
download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token)
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
@@ -520,23 +548,21 @@ def install_agent(request):
|
||||
rdp=request.data["rdp"],
|
||||
ping=request.data["ping"],
|
||||
power=request.data["power"],
|
||||
arch=arch,
|
||||
goarch=goarch,
|
||||
token=token,
|
||||
api=request.data["api"],
|
||||
file_name=request.data["fileName"],
|
||||
)
|
||||
|
||||
elif request.data["installMethod"] == "linux":
|
||||
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
|
||||
|
||||
code_token = CodeSignToken.objects.first()
|
||||
if not code_token:
|
||||
return notify_error("Missing code signing token")
|
||||
|
||||
if not code_token.is_valid:
|
||||
return notify_error("Code signing token is not valid")
|
||||
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
|
||||
|
||||
@@ -544,7 +570,7 @@ def install_agent(request):
|
||||
client=str(client_id),
|
||||
site=str(site_id),
|
||||
agent_type=request.data["agenttype"],
|
||||
arch=arch,
|
||||
arch=goarch,
|
||||
token=token,
|
||||
api=request.data["api"],
|
||||
download_url=download_url,
|
||||
@@ -639,28 +665,23 @@ def install_agent(request):
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RecoverAgentPerms])
|
||||
def recover(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
def recover(request, agent_id: str) -> Response:
|
||||
agent: Agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
|
||||
)
|
||||
mode = request.data["mode"]
|
||||
|
||||
if mode == "tacagent":
|
||||
if agent.is_posix:
|
||||
cmd = "systemctl restart tacticalagent.service"
|
||||
shell = 3
|
||||
else:
|
||||
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
|
||||
shell = 1
|
||||
uri = get_mesh_ws_url()
|
||||
asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, shell, 0))
|
||||
agent.recover(mode, uri, wait=False)
|
||||
return Response("Recovery will be attempted shortly")
|
||||
|
||||
elif mode == "mesh":
|
||||
data = {"func": "recover", "payload": {"mode": mode}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=20))
|
||||
if r == "ok":
|
||||
return Response("Successfully completed recovery")
|
||||
r, err = agent.recover(mode, "")
|
||||
if err:
|
||||
return notify_error(f"Unable to complete recovery: {r}")
|
||||
|
||||
return notify_error("Something went wrong")
|
||||
return Response("Successfully completed recovery")
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@@ -681,7 +702,7 @@ def run_script(request, agent_id):
|
||||
|
||||
hist = AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type="script_run",
|
||||
type=AgentHistoryType.SCRIPT_RUN,
|
||||
script=script,
|
||||
username=request.user.username[:50],
|
||||
)
|
||||
@@ -721,11 +742,11 @@ def run_script(request, agent_id):
|
||||
|
||||
custom_field = CustomField.objects.get(pk=request.data["custom_field"])
|
||||
|
||||
if custom_field.model == "agent":
|
||||
if custom_field.model == CustomFieldModel.AGENT:
|
||||
field = custom_field.get_or_create_field_value(agent)
|
||||
elif custom_field.model == "client":
|
||||
elif custom_field.model == CustomFieldModel.CLIENT:
|
||||
field = custom_field.get_or_create_field_value(agent.client)
|
||||
elif custom_field.model == "site":
|
||||
elif custom_field.model == CustomFieldModel.SITE:
|
||||
field = custom_field.get_or_create_field_value(agent.site)
|
||||
else:
|
||||
return notify_error("Custom Field was invalid")
|
||||
@@ -853,14 +874,14 @@ def bulk(request):
|
||||
return notify_error("Something went wrong")
|
||||
|
||||
if request.data["monType"] == "servers":
|
||||
q = q.filter(monitoring_type="server")
|
||||
q = q.filter(monitoring_type=AgentMonType.SERVER)
|
||||
elif request.data["monType"] == "workstations":
|
||||
q = q.filter(monitoring_type="workstation")
|
||||
q = q.filter(monitoring_type=AgentMonType.WORKSTATION)
|
||||
|
||||
if request.data["osType"] == "windows":
|
||||
q = q.filter(plat="windows")
|
||||
elif request.data["osType"] == "linux":
|
||||
q = q.filter(plat="linux")
|
||||
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]
|
||||
|
||||
@@ -951,6 +972,13 @@ 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):
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def delete_alerts_without_agent(apps, schema):
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,14 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Union, Optional, Dict, Any, List, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.db.models.fields import BooleanField, PositiveIntegerField
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,20 +25,6 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
@@ -57,7 +50,7 @@ class Alert(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
alert_type = models.CharField(
|
||||
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
|
||||
max_length=20, choices=AlertType.choices, default=AlertType.AVAILABILITY
|
||||
)
|
||||
message = models.TextField(null=True, blank=True)
|
||||
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
@@ -65,7 +58,9 @@ class Alert(models.Model):
|
||||
snooze_until = models.DateTimeField(null=True, blank=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
resolved_on = models.DateTimeField(null=True, blank=True)
|
||||
severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
|
||||
severity = models.CharField(
|
||||
max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO
|
||||
)
|
||||
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)
|
||||
@@ -74,21 +69,21 @@ class Alert(models.Model):
|
||||
action_run = models.DateTimeField(null=True, blank=True)
|
||||
action_stdout = models.TextField(null=True, blank=True)
|
||||
action_stderr = models.TextField(null=True, blank=True)
|
||||
action_retcode = models.IntegerField(null=True, blank=True)
|
||||
action_retcode = models.BigIntegerField(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_stdout = models.TextField(null=True, blank=True)
|
||||
resolved_action_stderr = models.TextField(null=True, blank=True)
|
||||
resolved_action_retcode = models.IntegerField(null=True, blank=True)
|
||||
resolved_action_retcode = models.BigIntegerField(null=True, blank=True)
|
||||
resolved_action_execution_time = models.CharField(
|
||||
max_length=100, null=True, blank=True
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
return f"{self.alert_type} - {self.message}"
|
||||
|
||||
@property
|
||||
def assigned_agent(self) -> "Agent":
|
||||
def assigned_agent(self) -> "Optional[Agent]":
|
||||
return self.agent
|
||||
|
||||
@property
|
||||
@@ -111,7 +106,7 @@ class Alert(models.Model):
|
||||
cls, agent: Agent, skip_create: bool = False
|
||||
) -> Optional[Alert]:
|
||||
if not cls.objects.filter(
|
||||
agent=agent, alert_type="availability", resolved=False
|
||||
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
|
||||
).exists():
|
||||
if skip_create:
|
||||
return None
|
||||
@@ -120,8 +115,8 @@ class Alert(models.Model):
|
||||
Alert,
|
||||
cls.objects.create(
|
||||
agent=agent,
|
||||
alert_type="availability",
|
||||
severity="error",
|
||||
alert_type=AlertType.AVAILABILITY,
|
||||
severity=AlertSeverity.ERROR,
|
||||
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
|
||||
hidden=True,
|
||||
),
|
||||
@@ -131,12 +126,12 @@ class Alert(models.Model):
|
||||
return cast(
|
||||
Alert,
|
||||
cls.objects.get(
|
||||
agent=agent, alert_type="availability", resolved=False
|
||||
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
|
||||
),
|
||||
)
|
||||
except cls.MultipleObjectsReturned:
|
||||
alerts = cls.objects.filter(
|
||||
agent=agent, alert_type="availability", resolved=False
|
||||
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
|
||||
)
|
||||
|
||||
last_alert = cast(Alert, alerts.last())
|
||||
@@ -173,10 +168,15 @@ class Alert(models.Model):
|
||||
cls.objects.create(
|
||||
assigned_check=check,
|
||||
agent=agent,
|
||||
alert_type="check",
|
||||
alert_type=AlertType.CHECK,
|
||||
severity=check.alert_severity
|
||||
if check.check_type
|
||||
not in ["memory", "cpuload", "diskspace", "script"]
|
||||
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,
|
||||
@@ -230,7 +230,7 @@ class Alert(models.Model):
|
||||
cls.objects.create(
|
||||
assigned_task=task,
|
||||
agent=agent,
|
||||
alert_type="task",
|
||||
alert_type=AlertType.TASK,
|
||||
severity=task.alert_severity,
|
||||
message=f"{agent.hostname} has task: {task.name} that failed.",
|
||||
hidden=True,
|
||||
@@ -296,14 +296,14 @@ class Alert(models.Model):
|
||||
dashboard_alert = instance.overdue_dashboard_alert
|
||||
alert_template = instance.alert_template
|
||||
maintenance_mode = instance.maintenance_mode
|
||||
alert_severity = "error"
|
||||
alert_severity = AlertSeverity.ERROR
|
||||
agent = instance
|
||||
dashboard_severities = [AlertSeverity.ERROR]
|
||||
email_severities = [AlertSeverity.ERROR]
|
||||
text_severities = [AlertSeverity.ERROR]
|
||||
|
||||
# set alert_template settings
|
||||
if alert_template:
|
||||
dashboard_severities = ["error"]
|
||||
email_severities = ["error"]
|
||||
text_severities = ["error"]
|
||||
always_dashboard = alert_template.agent_always_alert
|
||||
always_email = alert_template.agent_always_email
|
||||
always_text = alert_template.agent_always_text
|
||||
@@ -327,16 +327,37 @@ class Alert(models.Model):
|
||||
alert_severity = (
|
||||
instance.assigned_check.alert_severity
|
||||
if instance.assigned_check.check_type
|
||||
not in ["memory", "cpuload", "diskspace", "script"]
|
||||
not in [
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
else instance.alert_severity
|
||||
)
|
||||
agent = instance.agent
|
||||
|
||||
# set alert_template settings
|
||||
if alert_template:
|
||||
dashboard_severities = alert_template.check_dashboard_alert_severity
|
||||
email_severities = alert_template.check_email_alert_severity
|
||||
text_severities = alert_template.check_text_alert_severity
|
||||
dashboard_severities = (
|
||||
alert_template.check_dashboard_alert_severity
|
||||
if alert_template.check_dashboard_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_email = alert_template.check_always_email
|
||||
always_text = alert_template.check_always_text
|
||||
@@ -359,9 +380,21 @@ class Alert(models.Model):
|
||||
|
||||
# set alert_template settings
|
||||
if alert_template:
|
||||
dashboard_severities = alert_template.task_dashboard_alert_severity
|
||||
email_severities = alert_template.task_email_alert_severity
|
||||
text_severities = alert_template.task_text_alert_severity
|
||||
dashboard_severities = (
|
||||
alert_template.task_dashboard_alert_severity
|
||||
if alert_template.task_dashboard_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
email_severities = (
|
||||
alert_template.task_email_alert_severity
|
||||
if alert_template.task_email_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
text_severities = (
|
||||
alert_template.task_text_alert_severity
|
||||
if alert_template.task_text_alert_severity
|
||||
else [AlertSeverity.ERROR, AlertSeverity.WARNING]
|
||||
)
|
||||
always_dashboard = alert_template.task_always_alert
|
||||
always_email = alert_template.task_always_email
|
||||
always_text = alert_template.task_always_text
|
||||
@@ -449,7 +482,7 @@ class Alert(models.Model):
|
||||
else:
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type="scripting",
|
||||
log_type=DebugLogType.SCRIPTING,
|
||||
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
|
||||
)
|
||||
|
||||
@@ -573,7 +606,7 @@ class Alert(models.Model):
|
||||
else:
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type="scripting",
|
||||
log_type=DebugLogType.SCRIPTING,
|
||||
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
|
||||
)
|
||||
|
||||
@@ -600,7 +633,7 @@ class Alert(models.Model):
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
|
||||
except Exception as e:
|
||||
DebugLog.error(log_type="scripting", message=str(e))
|
||||
DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e))
|
||||
continue
|
||||
|
||||
else:
|
||||
@@ -670,17 +703,17 @@ class AlertTemplate(BaseAuditModel):
|
||||
|
||||
# check alert settings
|
||||
check_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
@@ -694,17 +727,17 @@ class AlertTemplate(BaseAuditModel):
|
||||
|
||||
# task alert settings
|
||||
task_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
@@ -738,9 +771,9 @@ class AlertTemplate(BaseAuditModel):
|
||||
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 == "workstation"
|
||||
or agent.monitoring_type == AgentMonType.WORKSTATION
|
||||
and self.exclude_workstations
|
||||
or agent.monitoring_type == "server"
|
||||
or agent.monitoring_type == AgentMonType.SERVER
|
||||
and self.exclude_servers
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import permissions
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from automation.serializers import PolicySerializer
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
|
||||
|
||||
from automation.serializers import PolicySerializer
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.utils import timezone as djangotime
|
||||
from .models import Alert
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
from .models import Alert
|
||||
|
||||
|
||||
@app.task
|
||||
def unsnooze_alerts() -> str:
|
||||
|
||||
@@ -2,16 +2,17 @@ from datetime import datetime, timedelta
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from core.utils import get_core_settings
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from autotasks.models import TaskResult
|
||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
|
||||
from core.utils import get_core_settings
|
||||
from tacticalrmm.constants import AgentMonType, AlertSeverity, AlertType, CheckStatus
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from autotasks.models import TaskResult
|
||||
from .models import Alert, AlertTemplate
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
@@ -39,14 +40,14 @@ class TestAlertsViews(TacticalTestCase):
|
||||
"alerts.Alert",
|
||||
agent=agent,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
severity="warning",
|
||||
severity=AlertSeverity.WARNING,
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
assigned_check=check,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
severity="error",
|
||||
severity=AlertSeverity.ERROR,
|
||||
_quantity=7,
|
||||
)
|
||||
baker.make(
|
||||
@@ -70,8 +71,8 @@ class TestAlertsViews(TacticalTestCase):
|
||||
data = {"top": 3}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
|
||||
self.assertEquals(resp.data["alerts_count"], 10)
|
||||
self.assertEqual(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
|
||||
self.assertEqual(resp.data["alerts_count"], 10)
|
||||
|
||||
# test filter data
|
||||
# test data and result counts
|
||||
@@ -391,8 +392,10 @@ class TestAlertTasks(TacticalTestCase):
|
||||
|
||||
core = get_core_settings()
|
||||
# setup data
|
||||
workstation = baker.make_recipe("agents.agent", monitoring_type="workstation")
|
||||
server = baker.make_recipe("agents.agent", monitoring_type="server")
|
||||
workstation = baker.make_recipe(
|
||||
"agents.agent", monitoring_type=AgentMonType.WORKSTATION
|
||||
)
|
||||
server = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER)
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
|
||||
@@ -409,15 +412,15 @@ class TestAlertTasks(TacticalTestCase):
|
||||
core.server_policy = policy
|
||||
core.save()
|
||||
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
|
||||
# assign second Alert Template to as default alert template
|
||||
core.alert_template = alert_templates[1]
|
||||
core.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[1].pk)
|
||||
|
||||
# assign third Alert Template to client
|
||||
workstation.client.alert_template = alert_templates[2]
|
||||
@@ -425,8 +428,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.client.save()
|
||||
server.client.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
|
||||
|
||||
# apply policy to client and should override
|
||||
workstation.client.workstation_policy = policy
|
||||
@@ -434,8 +437,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.client.save()
|
||||
server.client.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
|
||||
# assign fouth Alert Template to site
|
||||
workstation.site.alert_template = alert_templates[3]
|
||||
@@ -443,8 +446,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.site.save()
|
||||
server.site.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
|
||||
# apply policy to site
|
||||
workstation.site.workstation_policy = policy
|
||||
@@ -452,8 +455,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.site.save()
|
||||
server.site.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
|
||||
# apply policy to agents
|
||||
workstation.policy = policy
|
||||
@@ -461,35 +464,35 @@ class TestAlertTasks(TacticalTestCase):
|
||||
workstation.save()
|
||||
server.save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
|
||||
|
||||
# test disabling alert template
|
||||
alert_templates[0].is_active = False
|
||||
alert_templates[0].save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
|
||||
# test policy exclusions
|
||||
alert_templates[3].excluded_agents.set([workstation.pk])
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
|
||||
# test workstation exclusions
|
||||
alert_templates[2].exclude_workstations = True
|
||||
alert_templates[2].save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
|
||||
|
||||
# test server exclusions
|
||||
alert_templates[3].exclude_servers = True
|
||||
alert_templates[3].save()
|
||||
|
||||
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
|
||||
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
|
||||
self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
|
||||
|
||||
@patch("agents.tasks.sleep")
|
||||
@patch("core.models.CoreSettings.send_mail")
|
||||
@@ -523,7 +526,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
# call outages task and no alert should be created
|
||||
agent_outages_task()
|
||||
|
||||
self.assertEquals(Alert.objects.count(), 0)
|
||||
self.assertEqual(Alert.objects.count(), 0)
|
||||
|
||||
# set overdue_dashboard_alert and alert should be created
|
||||
agent_dashboard_alert.overdue_dashboard_alert = True
|
||||
@@ -574,22 +577,22 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent_outages_task()
|
||||
|
||||
# should have created 6 alerts
|
||||
self.assertEquals(Alert.objects.count(), 6)
|
||||
self.assertEqual(Alert.objects.count(), 6)
|
||||
|
||||
# other specific agents should have created alerts
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_text_alert).count(), 1)
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_email_alert).count(), 1)
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_template_email).count(), 1)
|
||||
self.assertEquals(
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_text_alert).count(), 1)
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_email_alert).count(), 1)
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_template_email).count(), 1)
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(agent=agent_template_dashboard).count(), 1
|
||||
)
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_template_text).count(), 1)
|
||||
self.assertEquals(Alert.objects.filter(agent=agent_template_blank).count(), 0)
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_template_text).count(), 1)
|
||||
self.assertEqual(Alert.objects.filter(agent=agent_template_blank).count(), 0)
|
||||
|
||||
# check if email and text tasks were called
|
||||
self.assertEquals(outage_email.call_count, 2)
|
||||
self.assertEquals(outage_sms.call_count, 2)
|
||||
self.assertEqual(outage_email.call_count, 2)
|
||||
self.assertEqual(outage_sms.call_count, 2)
|
||||
|
||||
outage_sms.assert_any_call(
|
||||
pk=Alert.objects.get(agent=agent_text_alert).pk, alert_interval=None
|
||||
@@ -630,7 +633,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
|
||||
# calling agent outage task again shouldn't create duplicate alerts and won't send alerts
|
||||
agent_outages_task()
|
||||
self.assertEquals(Alert.objects.count(), 6)
|
||||
self.assertEqual(Alert.objects.count(), 6)
|
||||
|
||||
# test periodic notification
|
||||
# change email/text sent to sometime in the past
|
||||
@@ -753,7 +756,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"alerts.AlertTemplate",
|
||||
is_active=True,
|
||||
check_always_email=True,
|
||||
check_email_alert_severity=["warning"],
|
||||
check_email_alert_severity=[AlertSeverity.WARNING],
|
||||
)
|
||||
agent_template_email.client.alert_template = alert_template_email
|
||||
agent_template_email.client.save()
|
||||
@@ -764,8 +767,12 @@ class TestAlertTasks(TacticalTestCase):
|
||||
is_active=True,
|
||||
check_always_alert=True,
|
||||
check_always_text=True,
|
||||
check_dashboard_alert_severity=["info", "warning", "error"],
|
||||
check_text_alert_severity=["error"],
|
||||
check_dashboard_alert_severity=[
|
||||
AlertSeverity.INFO,
|
||||
AlertSeverity.WARNING,
|
||||
AlertSeverity.ERROR,
|
||||
],
|
||||
check_text_alert_severity=[AlertSeverity.ERROR],
|
||||
)
|
||||
agent_template_dashboard_text.client.alert_template = (
|
||||
alert_template_dashboard_text
|
||||
@@ -789,7 +796,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"checks.CheckResult",
|
||||
assigned_check=check_agent,
|
||||
agent=agent,
|
||||
alert_severity="warning",
|
||||
alert_severity=AlertSeverity.WARNING,
|
||||
)
|
||||
check_template_email = baker.make_recipe(
|
||||
"checks.cpuload_check", agent=agent_template_email
|
||||
@@ -840,8 +847,8 @@ class TestAlertTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test agent with check that has alert settings
|
||||
check_agent_result.alert_severity = "warning"
|
||||
check_agent_result.status = "failing"
|
||||
check_agent_result.alert_severity = AlertSeverity.WARNING
|
||||
check_agent_result.status = CheckStatus.FAILING
|
||||
|
||||
Alert.handle_alert_failure(check_agent_result)
|
||||
|
||||
@@ -902,7 +909,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
outage_sms.assert_not_called
|
||||
|
||||
# update check alert severity to error
|
||||
check_template_dashboard_text_result.alert_severity = "error"
|
||||
check_template_dashboard_text_result.alert_severity = AlertSeverity.ERROR
|
||||
check_template_dashboard_text_result.save()
|
||||
|
||||
# now should trigger alert
|
||||
@@ -942,7 +949,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email.assert_not_called()
|
||||
send_sms.assert_not_called()
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(assigned_check=check_template_email).count(), 1
|
||||
)
|
||||
|
||||
@@ -1061,7 +1068,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
"alerts.AlertTemplate",
|
||||
is_active=True,
|
||||
task_always_email=True,
|
||||
task_email_alert_severity=["warning"],
|
||||
task_email_alert_severity=[AlertSeverity.WARNING],
|
||||
)
|
||||
agent_template_email.client.alert_template = alert_template_email
|
||||
agent_template_email.client.save()
|
||||
@@ -1072,8 +1079,12 @@ class TestAlertTasks(TacticalTestCase):
|
||||
is_active=True,
|
||||
task_always_alert=True,
|
||||
task_always_text=True,
|
||||
task_dashboard_alert_severity=["info", "warning", "error"],
|
||||
task_text_alert_severity=["error"],
|
||||
task_dashboard_alert_severity=[
|
||||
AlertSeverity.INFO,
|
||||
AlertSeverity.WARNING,
|
||||
AlertSeverity.ERROR,
|
||||
],
|
||||
task_text_alert_severity=[AlertSeverity.ERROR],
|
||||
)
|
||||
agent_template_dashboard_text.client.alert_template = (
|
||||
alert_template_dashboard_text
|
||||
@@ -1092,7 +1103,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
email_alert=True,
|
||||
text_alert=True,
|
||||
dashboard_alert=True,
|
||||
alert_severity="warning",
|
||||
alert_severity=AlertSeverity.WARNING,
|
||||
)
|
||||
task_agent_result = baker.make(
|
||||
"autotasks.TaskResult", agent=agent, task=task_agent
|
||||
@@ -1100,7 +1111,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
task_template_email = baker.make(
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent_template_email,
|
||||
alert_severity="warning",
|
||||
alert_severity=AlertSeverity.WARNING,
|
||||
)
|
||||
task_template_email_result = baker.make(
|
||||
"autotasks.TaskResult", agent=agent_template_email, task=task_template_email
|
||||
@@ -1108,7 +1119,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
task_template_dashboard_text = baker.make(
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent_template_dashboard_text,
|
||||
alert_severity="info",
|
||||
alert_severity=AlertSeverity.INFO,
|
||||
)
|
||||
task_template_dashboard_text_result = baker.make(
|
||||
"autotasks.TaskResult",
|
||||
@@ -1118,13 +1129,15 @@ class TestAlertTasks(TacticalTestCase):
|
||||
task_template_blank = baker.make(
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent_template_blank,
|
||||
alert_severity="error",
|
||||
alert_severity=AlertSeverity.ERROR,
|
||||
)
|
||||
task_template_blank_result = baker.make(
|
||||
"autotasks.TaskResult", agent=agent_template_blank, task=task_template_blank
|
||||
)
|
||||
task_no_settings = baker.make(
|
||||
"autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning"
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent_no_settings,
|
||||
alert_severity=AlertSeverity.WARNING,
|
||||
)
|
||||
task_no_settings_result = baker.make(
|
||||
"autotasks.TaskResult", agent=agent_no_settings, task=task_no_settings
|
||||
@@ -1202,7 +1215,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
outage_sms.assert_not_called
|
||||
|
||||
# update task alert seveity to error
|
||||
task_template_dashboard_text.alert_severity = "error"
|
||||
task_template_dashboard_text.alert_severity = AlertSeverity.ERROR
|
||||
task_template_dashboard_text.save()
|
||||
|
||||
# now should trigger alert
|
||||
@@ -1244,7 +1257,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
send_email.assert_not_called()
|
||||
send_sms.assert_not_called()
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(assigned_task=task_template_email).count(), 1
|
||||
)
|
||||
|
||||
@@ -1509,22 +1522,25 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="task",
|
||||
alert_type=AlertType.TASK,
|
||||
agent=cycle(agents),
|
||||
assigned_task=cycle(tasks),
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="check",
|
||||
alert_type=AlertType.CHECK,
|
||||
agent=cycle(agents),
|
||||
assigned_check=cycle(checks),
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
|
||||
"alerts.Alert",
|
||||
alert_type=AlertType.AVAILABILITY,
|
||||
agent=cycle(agents),
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make("alerts.Alert", alert_type="custom", _quantity=4)
|
||||
baker.make("alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4)
|
||||
|
||||
# test super user access
|
||||
r = self.check_authorized_superuser("patch", f"{base_url}/")
|
||||
@@ -1568,22 +1584,27 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
|
||||
alert_tasks = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="task",
|
||||
alert_type=AlertType.TASK,
|
||||
agent=cycle(agents),
|
||||
assigned_task=cycle(tasks),
|
||||
_quantity=3,
|
||||
)
|
||||
alert_checks = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="check",
|
||||
alert_type=AlertType.CHECK,
|
||||
agent=cycle(agents),
|
||||
assigned_check=cycle(checks),
|
||||
_quantity=3,
|
||||
)
|
||||
alert_agents = baker.make(
|
||||
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
|
||||
"alerts.Alert",
|
||||
alert_type=AlertType.AVAILABILITY,
|
||||
agent=cycle(agents),
|
||||
_quantity=3,
|
||||
)
|
||||
alert_custom = baker.make(
|
||||
"alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4
|
||||
)
|
||||
alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
|
||||
|
||||
# alert task url
|
||||
task_url = f"{base_url}/{alert_tasks[0].id}/" # for agent
|
||||
@@ -1660,7 +1681,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
alerts = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="availability",
|
||||
alert_type=AlertType.AVAILABILITY,
|
||||
agent=agent,
|
||||
resolved=False,
|
||||
_quantity=3,
|
||||
@@ -1674,7 +1695,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
# make sure only 1 alert is not resolved
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(
|
||||
alert_type="availability", agent=agent, resolved=False
|
||||
alert_type=AlertType.AVAILABILITY, agent=agent, resolved=False
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
@@ -1684,7 +1705,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
alerts = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="check",
|
||||
alert_type=AlertType.CHECK,
|
||||
assigned_check=check,
|
||||
agent=agent,
|
||||
resolved=False,
|
||||
@@ -1699,7 +1720,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
# make sure only 1 alert is not resolved
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(
|
||||
alert_type="check", agent=agent, resolved=False
|
||||
alert_type=AlertType.CHECK, agent=agent, resolved=False
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
@@ -1709,7 +1730,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
alerts = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="task",
|
||||
alert_type=AlertType.TASK,
|
||||
assigned_task=task,
|
||||
agent=agent,
|
||||
resolved=False,
|
||||
@@ -1724,7 +1745,7 @@ class TestAlertPermissions(TacticalTestCase):
|
||||
# make sure only 1 alert is not resolved
|
||||
self.assertEqual(
|
||||
Alert.objects.filter(
|
||||
alert_type="task", agent=agent, resolved=False
|
||||
alert_type=AlertType.TASK, agent=agent, resolved=False
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.helpers import notify_error
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .permissions import AlertPerms, AlertTemplatePerms
|
||||
|
||||
0
api/tacticalrmm/apiv3/tests/__init__.py
Normal file
0
api/tacticalrmm/apiv3/tests/__init__.py
Normal file
@@ -1,11 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from autotasks.models import TaskResult
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from autotasks.models import TaskResult
|
||||
from tacticalrmm.constants import CustomFieldModel, CustomFieldType, TaskStatus
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
|
||||
@@ -62,7 +59,7 @@ class TestAPIv3(TacticalTestCase):
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["check_interval"], 20)
|
||||
self.assertEquals(len(r.data["checks"]), 2)
|
||||
self.assertEqual(len(r.data["checks"]), 2)
|
||||
|
||||
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
||||
r = self.client.get(url)
|
||||
@@ -70,24 +67,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
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):
|
||||
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
|
||||
r = self.client.get(url, format="json")
|
||||
@@ -137,8 +116,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
self.assertEqual(len(r.json()["checks"]), 15)
|
||||
|
||||
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)
|
||||
|
||||
@@ -163,7 +140,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskGOGetSerializer(task).data, r.data)
|
||||
|
||||
def test_task_runner_results(self):
|
||||
from agents.models import AgentCustomField
|
||||
@@ -188,7 +164,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "passing")
|
||||
self.assertTrue(
|
||||
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.PASSING
|
||||
)
|
||||
|
||||
# test failing task
|
||||
data = {
|
||||
@@ -200,15 +178,28 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
|
||||
self.assertTrue(
|
||||
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
|
||||
)
|
||||
|
||||
# test collector task
|
||||
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
|
||||
text = baker.make(
|
||||
"core.CustomField",
|
||||
model=CustomFieldModel.AGENT,
|
||||
type=CustomFieldType.TEXT,
|
||||
name="Test",
|
||||
)
|
||||
boolean = baker.make(
|
||||
"core.CustomField", model="agent", type="checkbox", name="Test1"
|
||||
"core.CustomField",
|
||||
model=CustomFieldModel.AGENT,
|
||||
type=CustomFieldType.CHECKBOX,
|
||||
name="Test1",
|
||||
)
|
||||
multiple = baker.make(
|
||||
"core.CustomField", model="agent", type="multiple", name="Test2"
|
||||
"core.CustomField",
|
||||
model=CustomFieldModel.AGENT,
|
||||
type=CustomFieldType.MULTIPLE,
|
||||
name="Test2",
|
||||
)
|
||||
|
||||
# test text fields
|
||||
@@ -225,7 +216,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
|
||||
self.assertTrue(
|
||||
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
|
||||
)
|
||||
|
||||
# test saving to text field
|
||||
data = {
|
||||
@@ -237,7 +230,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
|
||||
)
|
||||
self.assertEqual(
|
||||
AgentCustomField.objects.get(field=text, agent=task.agent).value,
|
||||
"the last line",
|
||||
@@ -256,7 +251,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
|
||||
)
|
||||
self.assertTrue(
|
||||
AgentCustomField.objects.get(field=boolean, agent=task.agent).value
|
||||
)
|
||||
@@ -274,7 +271,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
|
||||
self.assertEqual(
|
||||
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"],
|
||||
@@ -290,7 +289,9 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
|
||||
)
|
||||
self.assertEqual(
|
||||
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
|
||||
["this"],
|
||||
@@ -9,7 +9,6 @@ urlpatterns = [
|
||||
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
|
||||
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
|
||||
path("meshexe/", views.MeshExe.as_view()),
|
||||
path("sysinfo/", views.SysInfo.as_view()),
|
||||
path("newagent/", views.NewAgent.as_view()),
|
||||
path("software/", views.Software.as_view()),
|
||||
path("installer/", views.Installer.as_view()),
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from accounts.models import User
|
||||
from agents.models import Agent, AgentHistory
|
||||
from agents.serializers import AgentHistorySerializer
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
|
||||
from checks.models import Check, CheckResult
|
||||
from checks.serializers import CheckRunnerGetSerializer
|
||||
from core.utils import get_core_settings
|
||||
from core.utils import download_mesh_agent, get_mesh_device_id, get_mesh_ws_url
|
||||
from django.conf import settings
|
||||
from django.db.models import Prefetch
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import DebugLog, PendingAction
|
||||
from packaging import version as pyver
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from software.models import InstalledSoftware
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
|
||||
from tacticalrmm.constants import MeshAgentIdent
|
||||
from tacticalrmm.utils import notify_error, reload_nats
|
||||
from accounts.models import User
|
||||
from agents.models import Agent, AgentHistory
|
||||
from agents.serializers import AgentHistorySerializer
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
|
||||
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
|
||||
from checks.models import Check, CheckResult
|
||||
from checks.serializers import CheckRunnerGetSerializer
|
||||
from core.utils import (
|
||||
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 tacticalrmm.constants import (
|
||||
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
|
||||
|
||||
|
||||
class CheckIn(APIView):
|
||||
@@ -34,7 +51,9 @@ class CheckIn(APIView):
|
||||
|
||||
# called once during tacticalagent windows service startup
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
if not agent.choco_installed:
|
||||
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
|
||||
|
||||
@@ -47,7 +66,9 @@ class SyncMeshNodeID(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
if agent.mesh_node_id != request.data["nodeid"]:
|
||||
agent.mesh_node_id = request.data["nodeid"]
|
||||
agent.save(update_fields=["mesh_node_id"])
|
||||
@@ -60,7 +81,9 @@ class Choco(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
agent.choco_installed = request.data["installed"]
|
||||
agent.save(update_fields=["choco_installed"])
|
||||
return Response("ok")
|
||||
@@ -71,7 +94,9 @@ class WinUpdates(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
|
||||
needs_reboot: bool = request.data["needs_reboot"]
|
||||
agent.needs_reboot = needs_reboot
|
||||
@@ -89,7 +114,7 @@ class WinUpdates(APIView):
|
||||
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type="windows_updates",
|
||||
log_type=DebugLogType.WIN_UPDATES,
|
||||
message=f"{agent.hostname} is rebooting after updates were installed.",
|
||||
)
|
||||
|
||||
@@ -97,7 +122,9 @@ class WinUpdates(APIView):
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
|
||||
if not u:
|
||||
raise WinUpdate.DoesNotExist
|
||||
@@ -124,8 +151,14 @@ class WinUpdates(APIView):
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
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:
|
||||
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
|
||||
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
|
||||
@@ -164,7 +197,9 @@ class SupersededWinUpdate(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
|
||||
for u in updates:
|
||||
u.delete()
|
||||
@@ -177,12 +212,19 @@ class RunChecks(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER).prefetch_related(
|
||||
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
|
||||
),
|
||||
agent_id=agentid,
|
||||
)
|
||||
checks = agent.get_checks_with_policies(exclude_overridden=True)
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_interval,
|
||||
"checks": CheckRunnerGetSerializer(checks, many=True).data,
|
||||
"checks": CheckRunnerGetSerializer(
|
||||
checks, context={"agent": agent}, many=True
|
||||
).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
@@ -192,7 +234,12 @@ class CheckRunner(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER).prefetch_related(
|
||||
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
|
||||
),
|
||||
agent_id=agentid,
|
||||
)
|
||||
checks = agent.get_checks_with_policies(exclude_overridden=True)
|
||||
|
||||
run_list = [
|
||||
@@ -216,30 +263,38 @@ class CheckRunner(APIView):
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_run_interval(),
|
||||
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
|
||||
"checks": CheckRunnerGetSerializer(
|
||||
run_list, context={"agent": agent}, many=True
|
||||
).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
def patch(self, request):
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
|
||||
if "agent_id" not in request.data.keys():
|
||||
return notify_error("Agent upgrade required")
|
||||
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
check = get_object_or_404(
|
||||
Check.objects.defer(*CHECK_DEFER),
|
||||
pk=request.data["id"],
|
||||
)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
||||
)
|
||||
|
||||
# check check result or create if doesn't exist
|
||||
try:
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=agent)
|
||||
except CheckResult.DoesNotExist:
|
||||
check_result = CheckResult(assigned_check=check, agent=agent)
|
||||
# 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,
|
||||
)
|
||||
|
||||
check_result.last_run = djangotime.now()
|
||||
check_result.save()
|
||||
if created:
|
||||
check_result.save()
|
||||
|
||||
status = check_result.handle_check(request.data)
|
||||
if status == "failing" and check.assignedtasks.exists(): # type: ignore
|
||||
for task in check.assignedtasks.all(): # type: ignore
|
||||
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)
|
||||
@@ -254,7 +309,10 @@ class CheckRunnerInterval(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"),
|
||||
agent_id=agentid,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
|
||||
@@ -266,19 +324,28 @@ class TaskRunner(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, agentid):
|
||||
_ = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid)
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
return Response(TaskGOGetSerializer(task).data)
|
||||
return Response(TaskGOGetSerializer(task, context={"agent": agent}).data)
|
||||
|
||||
def patch(self, request, pk, agentid):
|
||||
from alerts.models import Alert
|
||||
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.defer(*AGENT_DEFER),
|
||||
agent_id=agentid,
|
||||
)
|
||||
task = get_object_or_404(
|
||||
AutomatedTask.objects.select_related("custom_field"), pk=pk
|
||||
)
|
||||
|
||||
# check check result or create if doesn't exist
|
||||
# get task result or create if doesn't exist
|
||||
try:
|
||||
task_result = TaskResult.objects.get(task=task, agent=agent)
|
||||
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
|
||||
)
|
||||
@@ -290,7 +357,7 @@ class TaskRunner(APIView):
|
||||
|
||||
AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type="task_run",
|
||||
type=AuditActionType.TASK_RUN,
|
||||
command=task.name,
|
||||
script_results=request.data,
|
||||
)
|
||||
@@ -301,11 +368,13 @@ class TaskRunner(APIView):
|
||||
|
||||
task_result.save_collector_results()
|
||||
|
||||
status = "passing"
|
||||
status = CheckStatus.PASSING
|
||||
else:
|
||||
status = "failing"
|
||||
status = CheckStatus.FAILING
|
||||
else:
|
||||
status = "failing" if task_result.retcode != 0 else "passing"
|
||||
status = (
|
||||
CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
|
||||
)
|
||||
|
||||
if task_result:
|
||||
task_result.status = status
|
||||
@@ -314,7 +383,7 @@ class TaskRunner(APIView):
|
||||
task_result.status = status
|
||||
task.save(update_fields=["status"])
|
||||
|
||||
if status == "passing":
|
||||
if status == CheckStatus.PASSING:
|
||||
if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
|
||||
Alert.handle_alert_resolve(task_result)
|
||||
else:
|
||||
@@ -323,29 +392,14 @@ class TaskRunner(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
class MeshExe(APIView):
|
||||
"""Sends the mesh exe to the installer"""
|
||||
|
||||
def post(self, request):
|
||||
match request.data:
|
||||
case {"arch": "64", "plat": "windows"}:
|
||||
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
|
||||
arch = MeshAgentIdent.WIN64
|
||||
case {"arch": "32", "plat": "windows"}:
|
||||
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
|
||||
arch = MeshAgentIdent.WIN32
|
||||
case _:
|
||||
return notify_error("Arch not specified")
|
||||
@@ -403,7 +457,7 @@ class NewAgent(APIView):
|
||||
|
||||
token = Token.objects.create(user=user)
|
||||
|
||||
if agent.monitoring_type == "workstation":
|
||||
if agent.monitoring_type == AgentMonType.WORKSTATION:
|
||||
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
|
||||
else:
|
||||
WinUpdatePolicy(agent=agent).save()
|
||||
@@ -414,8 +468,8 @@ class NewAgent(APIView):
|
||||
AuditLog.objects.create(
|
||||
username=request.user,
|
||||
agent=agent.hostname,
|
||||
object_type="agent",
|
||||
action="agent_install",
|
||||
object_type=AuditObjType.AGENT,
|
||||
action=AuditActionType.AGENT_INSTALL,
|
||||
message=f"{request.user} installed new agent {agent.hostname}",
|
||||
after_value=Agent.serialize(agent),
|
||||
debug_info={"ip": request._client_ip},
|
||||
@@ -452,7 +506,10 @@ class Installer(APIView):
|
||||
return notify_error("Invalid data")
|
||||
|
||||
ver = request.data["version"]
|
||||
if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER):
|
||||
if (
|
||||
pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
and not "-dev" in settings.LATEST_AGENT_VER
|
||||
):
|
||||
return notify_error(
|
||||
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
|
||||
)
|
||||
@@ -487,7 +544,7 @@ class ChocoResult(APIView):
|
||||
|
||||
action.details["output"] = results
|
||||
action.details["installed"] = installed
|
||||
action.status = "completed"
|
||||
action.status = PAStatus.COMPLETED
|
||||
action.save(update_fields=["details", "status"])
|
||||
return Response("ok")
|
||||
|
||||
@@ -497,8 +554,9 @@ class AgentHistoryResult(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request, agentid, pk):
|
||||
_ = get_object_or_404(Agent, agent_id=agentid)
|
||||
hist = get_object_or_404(AgentHistory, pk=pk)
|
||||
hist = get_object_or_404(
|
||||
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
|
||||
)
|
||||
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
|
||||
from agents.models import Agent
|
||||
from clients.models import Client, Site
|
||||
from django.db import models
|
||||
from django.core.cache import cache
|
||||
from logs.models import BaseAuditModel
|
||||
|
||||
from typing import Optional, Dict, Any, List, TYPE_CHECKING
|
||||
|
||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY
|
||||
from tacticalrmm.constants import (
|
||||
CORESETTINGS_CACHE_KEY,
|
||||
AgentMonType,
|
||||
AgentPlat,
|
||||
CheckType,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from checks.models import Check
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
|
||||
|
||||
class Policy(BaseAuditModel):
|
||||
@@ -123,7 +128,7 @@ class Policy(BaseAuditModel):
|
||||
.exclude(id__in=excluded_agents_ids)
|
||||
.exclude(site_id__in=excluded_sites_ids)
|
||||
.exclude(site__client_id__in=excluded_clients_ids)
|
||||
.filter(monitoring_type="server")
|
||||
.filter(monitoring_type=AgentMonType.SERVER)
|
||||
.only("id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
@@ -136,7 +141,7 @@ class Policy(BaseAuditModel):
|
||||
.exclude(id__in=excluded_agents_ids)
|
||||
.exclude(site_id__in=excluded_sites_ids)
|
||||
.exclude(site__client_id__in=excluded_clients_ids)
|
||||
.filter(monitoring_type="workstation")
|
||||
.filter(monitoring_type=AgentMonType.WORKSTATION)
|
||||
.only("id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
@@ -155,7 +160,7 @@ class Policy(BaseAuditModel):
|
||||
explicit_clients_qs = Client.objects.none()
|
||||
explicit_sites_qs = Site.objects.none()
|
||||
|
||||
if not mon_type or mon_type == "workstation":
|
||||
if not mon_type or mon_type == AgentMonType.WORKSTATION:
|
||||
explicit_clients_qs |= self.workstation_clients.exclude( # type: ignore
|
||||
id__in=excluded_clients_ids
|
||||
)
|
||||
@@ -163,7 +168,7 @@ class Policy(BaseAuditModel):
|
||||
id__in=excluded_sites_ids
|
||||
)
|
||||
|
||||
if not mon_type or mon_type == "server":
|
||||
if not mon_type or mon_type == AgentMonType.SERVER:
|
||||
explicit_clients_qs |= self.server_clients.exclude( # type: ignore
|
||||
id__in=excluded_clients_ids
|
||||
)
|
||||
@@ -279,7 +284,10 @@ class Policy(BaseAuditModel):
|
||||
|
||||
# Loop over checks in with enforced policies first, then non-enforced policies
|
||||
for check in enforced_checks + agent_checks + policy_checks:
|
||||
if check.check_type == "diskspace" and agent.plat == "windows":
|
||||
if (
|
||||
check.check_type == CheckType.DISK_SPACE
|
||||
and agent.plat == AgentPlat.WINDOWS
|
||||
):
|
||||
# Check if drive letter was already added
|
||||
if check.disk not in added_diskspace_checks:
|
||||
added_diskspace_checks.append(check.disk)
|
||||
@@ -289,7 +297,7 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "ping":
|
||||
elif check.check_type == CheckType.PING:
|
||||
# Check if IP/host was already added
|
||||
if check.ip not in added_ping_checks:
|
||||
added_ping_checks.append(check.ip)
|
||||
@@ -299,7 +307,10 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "cpuload" and agent.plat == "windows":
|
||||
elif (
|
||||
check.check_type == CheckType.CPU_LOAD
|
||||
and agent.plat == AgentPlat.WINDOWS
|
||||
):
|
||||
# Check if cpuload list is empty
|
||||
if not added_cpuload_checks:
|
||||
added_cpuload_checks.append(check.pk)
|
||||
@@ -309,7 +320,9 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "memory" and agent.plat == "windows":
|
||||
elif (
|
||||
check.check_type == CheckType.MEMORY and agent.plat == AgentPlat.WINDOWS
|
||||
):
|
||||
# Check if memory check list is empty
|
||||
if not added_memory_checks:
|
||||
added_memory_checks.append(check.pk)
|
||||
@@ -319,7 +332,9 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "winsvc" and agent.plat == "windows":
|
||||
elif (
|
||||
check.check_type == CheckType.WINSVC and agent.plat == AgentPlat.WINDOWS
|
||||
):
|
||||
# Check if service name was already added
|
||||
if check.svc_name not in added_winsvc_checks:
|
||||
added_winsvc_checks.append(check.svc_name)
|
||||
@@ -329,7 +344,7 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "script" and agent.is_supported_script(
|
||||
elif check.check_type == CheckType.SCRIPT and agent.is_supported_script(
|
||||
check.script.supported_platforms
|
||||
):
|
||||
# Check if script id was already added
|
||||
@@ -341,7 +356,10 @@ class Policy(BaseAuditModel):
|
||||
elif check.agent:
|
||||
overridden_checks.append(check.pk)
|
||||
|
||||
elif check.check_type == "eventlog" and agent.plat == "windows":
|
||||
elif (
|
||||
check.check_type == CheckType.EVENT_LOG
|
||||
and agent.plat == AgentPlat.WINDOWS
|
||||
):
|
||||
# Check if events were already added
|
||||
if [check.log_name, check.event_id] not in added_eventlog_checks:
|
||||
added_eventlog_checks.append([check.log_name, check.event_id])
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
ReadOnlyField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from agents.models import Agent
|
||||
from core.utils import get_core_settings
|
||||
from model_bakery import baker, seq
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
from tacticalrmm.constants import AgentMonType, TaskSyncStatus
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
from .serializers import (
|
||||
PolicyCheckStatusSerializer,
|
||||
@@ -410,11 +411,11 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEquals(len(resp.data["server_clients"]), 1)
|
||||
self.assertEquals(len(resp.data["server_sites"]), 0)
|
||||
self.assertEquals(len(resp.data["workstation_clients"]), 1)
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 0)
|
||||
self.assertEquals(len(resp.data["agents"]), 0)
|
||||
self.assertEqual(len(resp.data["server_clients"]), 1)
|
||||
self.assertEqual(len(resp.data["server_sites"]), 0)
|
||||
self.assertEqual(len(resp.data["workstation_clients"]), 1)
|
||||
self.assertEqual(len(resp.data["workstation_sites"]), 0)
|
||||
self.assertEqual(len(resp.data["agents"]), 0)
|
||||
|
||||
# Add Site to Policy
|
||||
policy.server_sites.add(server_agents[10].site)
|
||||
@@ -422,9 +423,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
resp = self.client.get(
|
||||
f"/automation/policies/{policy.pk}/related/", format="json"
|
||||
)
|
||||
self.assertEquals(len(resp.data["server_sites"]), 1)
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 1)
|
||||
self.assertEquals(len(resp.data["agents"]), 0)
|
||||
self.assertEqual(len(resp.data["server_sites"]), 1)
|
||||
self.assertEqual(len(resp.data["workstation_sites"]), 1)
|
||||
self.assertEqual(len(resp.data["agents"]), 0)
|
||||
|
||||
# Add Agent to Policy
|
||||
policy.agents.add(server_agents[2])
|
||||
@@ -432,7 +433,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
resp = self.client.get(
|
||||
f"/automation/policies/{policy.pk}/related/", format="json"
|
||||
)
|
||||
self.assertEquals(len(resp.data["agents"]), 2)
|
||||
self.assertEqual(len(resp.data["agents"]), 2)
|
||||
|
||||
def test_getting_agent_policy_checks(self):
|
||||
|
||||
@@ -442,7 +443,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent", policy=policy)
|
||||
|
||||
# test policy assigned to agent
|
||||
self.assertEquals(len(agent.get_checks_from_policies()), 7)
|
||||
self.assertEqual(len(agent.get_checks_from_policies()), 7)
|
||||
|
||||
def test_getting_agent_policy_checks_with_enforced(self):
|
||||
# setup data
|
||||
@@ -496,7 +497,10 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
task_result = baker.make(
|
||||
"autotasks.TaskResult", task=task, agent=agent, sync_status="synced"
|
||||
"autotasks.TaskResult",
|
||||
task=task,
|
||||
agent=agent,
|
||||
sync_status=TaskSyncStatus.SYNCED,
|
||||
)
|
||||
|
||||
# this change shouldn't trigger the task_result field to sync_status = "notsynced"
|
||||
@@ -509,14 +513,15 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
task.save()
|
||||
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(pk=task_result.id).sync_status, "synced"
|
||||
TaskResult.objects.get(pk=task_result.id).sync_status, TaskSyncStatus.SYNCED
|
||||
)
|
||||
|
||||
# task result should now be "notsynced"
|
||||
task.enabled = False
|
||||
task.save()
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(pk=task_result.id).sync_status, "notsynced"
|
||||
TaskResult.objects.get(pk=task_result.id).sync_status,
|
||||
TaskSyncStatus.NOT_SYNCED,
|
||||
)
|
||||
|
||||
def test_policy_exclusions(self):
|
||||
@@ -526,7 +531,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
baker.make_recipe("autotasks.task", policy=policy)
|
||||
agent = baker.make_recipe(
|
||||
"agents.agent", policy=policy, monitoring_type="server"
|
||||
"agents.agent", policy=policy, monitoring_type=AgentMonType.SERVER
|
||||
)
|
||||
|
||||
checks = agent.get_checks_with_policies()
|
||||
@@ -622,7 +627,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
baker.make_recipe("autotasks.task", policy=policy)
|
||||
agent = baker.make_recipe("agents.agent", monitoring_type="server")
|
||||
agent = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER)
|
||||
|
||||
core = get_core_settings()
|
||||
core.server_policy = policy
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from autotasks.views import GetAddAutoTasks
|
||||
from checks.views import GetAddChecks
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from agents.models import Agent
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from autotasks.models import TaskResult
|
||||
from checks.models import CheckResult
|
||||
from clients.models import Client
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
|
||||
from .models import Policy
|
||||
from .permissions import AutomationPolicyPerms
|
||||
from .serializers import (
|
||||
@@ -28,7 +28,9 @@ class GetAddPolicies(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request):
|
||||
policies = Policy.objects.all()
|
||||
policies = Policy.objects.select_related("alert_template").prefetch_related(
|
||||
"excluded_agents", "excluded_sites", "excluded_clients"
|
||||
)
|
||||
|
||||
return Response(
|
||||
PolicyTableSerializer(
|
||||
@@ -106,7 +108,9 @@ class PolicyCheck(APIView):
|
||||
class OverviewPolicy(APIView):
|
||||
def get(self, request):
|
||||
|
||||
clients = Client.objects.all()
|
||||
clients = Client.objects.filter_by_role(request.user).select_related(
|
||||
"workstation_policy", "server_policy"
|
||||
)
|
||||
return Response(PolicyOverviewSerializer(clients, many=True).data)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from autotasks.tasks import remove_orphaned_win_tasks
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from autotasks.tasks import remove_orphaned_win_tasks
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Checks for orphaned tasks on all agents and removes them"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.2.12 on 2022-04-01 22:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
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")
|
||||
@@ -12,8 +14,8 @@ def migrate_script_data(apps, schema_editor):
|
||||
edited = False
|
||||
|
||||
# convert scheduled task_type
|
||||
if task.task_type == "scheduled":
|
||||
task.task_type = "daily"
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ 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):
|
||||
@@ -22,7 +23,9 @@ def check_for_win_task_name_duplicates(apps, schema_editor):
|
||||
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="notsynced")
|
||||
TaskResult.objects.filter(task=dups[x]).update(
|
||||
sync_status=TaskSyncStatus.NOT_SYNCED
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Generated by Django 4.0.3 on 2022-04-15 20:52
|
||||
|
||||
import autotasks.models
|
||||
from django.db import migrations, models
|
||||
|
||||
import autotasks.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +1,27 @@
|
||||
import asyncio
|
||||
import random
|
||||
import string
|
||||
import pytz
|
||||
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
import pytz
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone as djangotime
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models.fields import DateTimeField
|
||||
from django.db.models.fields.json import JSONField
|
||||
from django.db.utils import DatabaseError
|
||||
from logs.models import BaseAuditModel, DebugLog
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from core.utils import get_core_settings
|
||||
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:
|
||||
@@ -34,30 +39,6 @@ from tacticalrmm.utils import (
|
||||
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
|
||||
]
|
||||
|
||||
SYNC_STATUS_CHOICES = [
|
||||
("synced", "Synced With Agent"),
|
||||
("notsynced", "Waiting On Agent Checkin"),
|
||||
("pendingdeletion", "Pending Deletion on Agent"),
|
||||
("initial", "Initial Task Sync"),
|
||||
]
|
||||
|
||||
TASK_STATUS_CHOICES = [
|
||||
("passing", "Passing"),
|
||||
("failing", "Failing"),
|
||||
("pending", "Pending"),
|
||||
]
|
||||
|
||||
|
||||
def generate_task_name() -> str:
|
||||
chars = string.ascii_letters
|
||||
@@ -103,7 +84,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
enabled = models.BooleanField(default=True)
|
||||
continue_on_error = models.BooleanField(default=True)
|
||||
alert_severity = models.CharField(
|
||||
max_length=30, choices=SEVERITY_CHOICES, default="info"
|
||||
max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO
|
||||
)
|
||||
email_alert = models.BooleanField(default=False)
|
||||
text_alert = models.BooleanField(default=False)
|
||||
@@ -112,7 +93,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
# options sent to agent for task creation
|
||||
# general task settings
|
||||
task_type = models.CharField(
|
||||
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
|
||||
max_length=100, choices=TaskType.choices, default=TaskType.MANUAL
|
||||
)
|
||||
win_task_name = models.CharField(
|
||||
max_length=255, unique=True, blank=True, default=generate_task_name
|
||||
@@ -175,12 +156,14 @@ class AutomatedTask(BaseAuditModel):
|
||||
for field in self.fields_that_trigger_task_update_on_agent:
|
||||
if getattr(self, field) != getattr(old_task, field):
|
||||
if self.policy:
|
||||
TaskResult.objects.exclude(sync_status="initial").filter(
|
||||
task__policy_id=self.policy.id
|
||||
).update(sync_status="notsynced")
|
||||
TaskResult.objects.exclude(
|
||||
sync_status=TaskSyncStatus.INITIAL
|
||||
).filter(task__policy_id=self.policy.id).update(
|
||||
sync_status=TaskSyncStatus.NOT_SYNCED
|
||||
)
|
||||
else:
|
||||
TaskResult.objects.filter(agent=self.agent, task=self).update(
|
||||
sync_status="notsynced"
|
||||
sync_status=TaskSyncStatus.NOT_SYNCED
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
@@ -197,31 +180,31 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def schedule(self) -> Optional[str]:
|
||||
if self.task_type == "manual":
|
||||
if self.task_type == TaskType.MANUAL:
|
||||
return "Manual"
|
||||
elif self.task_type == "checkfailure":
|
||||
elif self.task_type == TaskType.CHECK_FAILURE:
|
||||
return "Every time check fails"
|
||||
elif self.task_type == "runonce":
|
||||
elif self.task_type == TaskType.RUN_ONCE:
|
||||
return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}'
|
||||
elif self.task_type == "daily":
|
||||
elif self.task_type == TaskType.DAILY:
|
||||
run_time_nice = self.run_time_date.strftime("%I:%M%p")
|
||||
if self.daily_interval == 1:
|
||||
return f"Daily at {run_time_nice}"
|
||||
else:
|
||||
return f"Every {self.daily_interval} days at {run_time_nice}"
|
||||
elif self.task_type == "weekly":
|
||||
elif self.task_type == TaskType.WEEKLY:
|
||||
run_time_nice = self.run_time_date.strftime("%I:%M%p")
|
||||
days = bitdays_to_string(self.run_time_bit_weekdays)
|
||||
if self.weekly_interval != 1:
|
||||
return f"{days} at {run_time_nice}"
|
||||
else:
|
||||
return f"{days} at {run_time_nice} every {self.weekly_interval} weeks"
|
||||
elif self.task_type == "monthly":
|
||||
elif self.task_type == TaskType.MONTHLY:
|
||||
run_time_nice = self.run_time_date.strftime("%I:%M%p")
|
||||
months = bitmonths_to_string(self.monthly_months_of_year)
|
||||
days = bitmonthdays_to_string(self.monthly_days_of_month)
|
||||
return f"Runs on {months} on days {days} at {run_time_nice}"
|
||||
elif self.task_type == "monthlydow":
|
||||
elif self.task_type == TaskType.MONTHLY_DOW:
|
||||
run_time_nice = self.run_time_date.strftime("%I:%M%p")
|
||||
months = bitmonths_to_string(self.monthly_months_of_year)
|
||||
weeks = bitweeks_to_string(self.monthly_weeks_of_month)
|
||||
@@ -265,7 +248,9 @@ class AutomatedTask(BaseAuditModel):
|
||||
"name": self.win_task_name,
|
||||
"overwrite_task": editing,
|
||||
"enabled": self.enabled,
|
||||
"trigger": self.task_type if self.task_type != "checkfailure" else "manual",
|
||||
"trigger": self.task_type
|
||||
if self.task_type != TaskType.CHECK_FAILURE
|
||||
else TaskType.MANUAL,
|
||||
"multiple_instances": self.task_instance_policy
|
||||
if self.task_instance_policy
|
||||
else 0,
|
||||
@@ -273,15 +258,21 @@ class AutomatedTask(BaseAuditModel):
|
||||
if self.expire_date
|
||||
else False,
|
||||
"start_when_available": self.run_asap_after_missed
|
||||
if self.task_type != "runonce"
|
||||
if self.task_type != TaskType.RUN_ONCE
|
||||
else True,
|
||||
}
|
||||
|
||||
if self.task_type in ["runonce", "daily", "weekly", "monthly", "monthlydow"]:
|
||||
if self.task_type in [
|
||||
TaskType.RUN_ONCE,
|
||||
TaskType.DAILY,
|
||||
TaskType.WEEKLY,
|
||||
TaskType.MONTHLY,
|
||||
TaskType.MONTHLY_DOW,
|
||||
]:
|
||||
# set runonce task in future if creating and run_asap_after_missed is set
|
||||
if (
|
||||
not editing
|
||||
and self.task_type == "runonce"
|
||||
and self.task_type == TaskType.RUN_ONCE
|
||||
and self.run_asap_after_missed
|
||||
and agent
|
||||
and self.run_time_date
|
||||
@@ -316,14 +307,14 @@ class AutomatedTask(BaseAuditModel):
|
||||
)
|
||||
task["stop_at_duration_end"] = self.stop_task_at_duration_end
|
||||
|
||||
if self.task_type == "daily":
|
||||
if self.task_type == TaskType.DAILY:
|
||||
task["day_interval"] = self.daily_interval
|
||||
|
||||
elif self.task_type == "weekly":
|
||||
elif self.task_type == TaskType.WEEKLY:
|
||||
task["week_interval"] = self.weekly_interval
|
||||
task["days_of_week"] = self.run_time_bit_weekdays
|
||||
|
||||
elif self.task_type == "monthly":
|
||||
elif self.task_type == TaskType.MONTHLY:
|
||||
|
||||
# check if "last day is configured"
|
||||
if self.monthly_days_of_month >= 0x80000000:
|
||||
@@ -335,7 +326,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
task["months_of_year"] = self.monthly_months_of_year
|
||||
|
||||
elif self.task_type == "monthlydow":
|
||||
elif self.task_type == TaskType.MONTHLY_DOW:
|
||||
task["days_of_week"] = self.run_time_bit_weekdays
|
||||
task["months_of_year"] = self.monthly_months_of_year
|
||||
task["weeks_of_month"] = self.monthly_weeks_of_month
|
||||
@@ -362,20 +353,20 @@ class AutomatedTask(BaseAuditModel):
|
||||
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
|
||||
|
||||
if r != "ok":
|
||||
task_result.sync_status = "initial"
|
||||
task_result.sync_status = TaskSyncStatus.INITIAL
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
DebugLog.warning(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.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.",
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
task_result.sync_status = "synced"
|
||||
task_result.sync_status = TaskSyncStatus.SYNCED
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"{task_result.agent.hostname} task {self.name} was successfully created",
|
||||
)
|
||||
|
||||
@@ -401,20 +392,20 @@ class AutomatedTask(BaseAuditModel):
|
||||
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
|
||||
|
||||
if r != "ok":
|
||||
task_result.sync_status = "notsynced"
|
||||
task_result.sync_status = TaskSyncStatus.NOT_SYNCED
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
DebugLog.warning(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.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",
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
task_result.sync_status = "synced"
|
||||
task_result.sync_status = TaskSyncStatus.SYNCED
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"{task_result.agent.hostname} task {self.name} was successfully modified",
|
||||
)
|
||||
|
||||
@@ -439,7 +430,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10))
|
||||
|
||||
if r != "ok" and "The system cannot find the file specified" not in r:
|
||||
task_result.sync_status = "pendingdeletion"
|
||||
task_result.sync_status = TaskSyncStatus.PENDING_DELETION
|
||||
|
||||
try:
|
||||
task_result.save(update_fields=["sync_status"])
|
||||
@@ -448,7 +439,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
DebugLog.warning(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin",
|
||||
)
|
||||
return "timeout"
|
||||
@@ -456,7 +447,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
self.delete()
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted",
|
||||
)
|
||||
|
||||
@@ -514,16 +505,16 @@ class TaskResult(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
retcode = models.IntegerField(null=True, blank=True)
|
||||
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=TASK_STATUS_CHOICES, default="pending"
|
||||
max_length=30, choices=TaskStatus.choices, default=TaskStatus.PENDING
|
||||
)
|
||||
sync_status = models.CharField(
|
||||
max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
|
||||
max_length=100, choices=TaskSyncStatus.choices, default=TaskSyncStatus.INITIAL
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from scripts.models import Script
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from tacticalrmm.constants import TaskType
|
||||
|
||||
from .models import AutomatedTask, TaskResult
|
||||
|
||||
@@ -9,6 +10,7 @@ class TaskResultSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TaskResult
|
||||
fields = "__all__"
|
||||
read_only_fields = ("agent", "task")
|
||||
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
@@ -97,7 +99,14 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
|
||||
# run_time_date required
|
||||
if (
|
||||
data["task_type"] in ["runonce", "daily", "weekly", "monthly", "monthlydow"]
|
||||
data["task_type"]
|
||||
in [
|
||||
TaskType.RUN_ONCE,
|
||||
TaskType.DAILY,
|
||||
TaskType.WEEKLY,
|
||||
TaskType.MONTHLY,
|
||||
TaskType.MONTHLY_DOW,
|
||||
]
|
||||
and not data["run_time_date"]
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -105,14 +114,14 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
# daily task type validation
|
||||
if data["task_type"] == "daily":
|
||||
if data["task_type"] == TaskType.DAILY:
|
||||
if "daily_interval" not in data or not data["daily_interval"]:
|
||||
raise serializers.ValidationError(
|
||||
f"daily_interval is required for task_type '{data['task_type']}'"
|
||||
)
|
||||
|
||||
# weekly task type validation
|
||||
elif data["task_type"] == "weekly":
|
||||
elif data["task_type"] == TaskType.WEEKLY:
|
||||
if "weekly_interval" not in data or not data["weekly_interval"]:
|
||||
raise serializers.ValidationError(
|
||||
f"weekly_interval is required for task_type '{data['task_type']}'"
|
||||
@@ -124,7 +133,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
# monthly task type validation
|
||||
elif data["task_type"] == "monthly":
|
||||
elif data["task_type"] == TaskType.MONTHLY:
|
||||
if (
|
||||
"monthly_months_of_year" not in data
|
||||
or not data["monthly_months_of_year"]
|
||||
@@ -139,7 +148,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
# monthly day of week task type validation
|
||||
elif data["task_type"] == "monthlydow":
|
||||
elif data["task_type"] == TaskType.MONTHLY_DOW:
|
||||
if (
|
||||
"monthly_months_of_year" not in data
|
||||
or not data["monthly_months_of_year"]
|
||||
@@ -162,7 +171,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
# check failure task type validation
|
||||
elif data["task_type"] == "checkfailure":
|
||||
elif data["task_type"] == TaskType.CHECK_FAILURE:
|
||||
if "assigned_check" not in data or not data["assigned_check"]:
|
||||
raise serializers.ValidationError(
|
||||
f"assigned_check is required for task_type '{data['task_type']}'"
|
||||
@@ -198,13 +207,14 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
def get_task_actions(self, obj):
|
||||
tmp = []
|
||||
actions_to_remove = []
|
||||
agent = self.context["agent"]
|
||||
for action in obj.actions:
|
||||
if action["type"] == "cmd":
|
||||
tmp.append(
|
||||
{
|
||||
"type": "cmd",
|
||||
"command": Script.parse_script_args(
|
||||
agent=obj.agent,
|
||||
agent=agent,
|
||||
shell=action["shell"],
|
||||
args=[action["command"]],
|
||||
)[0],
|
||||
@@ -215,7 +225,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
elif action["type"] == "script":
|
||||
try:
|
||||
script = Script.objects.get(pk=action["script"])
|
||||
except ObjectDoesNotExist:
|
||||
except Script.DoesNotExist:
|
||||
# script doesn't exist so remove it
|
||||
actions_to_remove.append(action["script"])
|
||||
continue
|
||||
@@ -225,7 +235,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
"script_name": script.name,
|
||||
"code": script.code,
|
||||
"script_args": Script.parse_script_args(
|
||||
agent=obj.agent,
|
||||
agent=agent,
|
||||
shell=script.shell,
|
||||
args=action["script_args"],
|
||||
),
|
||||
|
||||
@@ -2,15 +2,16 @@ import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
from time import sleep
|
||||
from typing import Union, Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from agents.models import Agent
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import DebugLog
|
||||
|
||||
from agents.models import Agent
|
||||
from alerts.models import Alert
|
||||
from autotasks.models import TaskResult
|
||||
from autotasks.models import AutomatedTask, TaskResult
|
||||
from logs.models import DebugLog
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.constants import DebugLogType
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -83,10 +84,10 @@ def remove_orphaned_win_tasks() -> None:
|
||||
if not isinstance(r, list): # empty list
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Unable to pull list of scheduled tasks on {agent.hostname}: {r}",
|
||||
)
|
||||
return
|
||||
continue
|
||||
|
||||
agent_task_names = [
|
||||
task.win_task_name for task in agent.get_tasks_with_policies()
|
||||
@@ -114,13 +115,13 @@ def remove_orphaned_win_tasks() -> None:
|
||||
if ret != "ok":
|
||||
DebugLog.error(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
|
||||
)
|
||||
else:
|
||||
DebugLog.info(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
log_type=DebugLogType.AGENT_ISSUES,
|
||||
message=f"Removed orphaned task {task} from {agent.hostname}",
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ from unittest.mock import call, patch
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from tacticalrmm.constants import TaskType
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import AutomatedTask, TaskResult
|
||||
from .models import AutomatedTask, TaskResult, TaskSyncStatus
|
||||
from .serializers import TaskSerializer
|
||||
from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task
|
||||
|
||||
@@ -454,7 +455,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 1",
|
||||
task_type="daily",
|
||||
task_type=TaskType.DAILY,
|
||||
daily_interval=1,
|
||||
run_time_date=djangotime.now() + djangotime.timedelta(hours=3, minutes=30),
|
||||
)
|
||||
@@ -487,13 +488,15 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
)
|
||||
nats_cmd.reset_mock()
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(task=task1, agent=agent).sync_status, "synced"
|
||||
TaskResult.objects.get(task=task1, agent=agent).sync_status,
|
||||
TaskSyncStatus.SYNCED,
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
create_win_task_schedule(pk=task1.pk)
|
||||
self.assertEqual(
|
||||
TaskResult.objects.get(task=task1, agent=agent).sync_status, "initial"
|
||||
TaskResult.objects.get(task=task1, agent=agent).sync_status,
|
||||
TaskSyncStatus.INITIAL,
|
||||
)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
@@ -502,7 +505,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 1",
|
||||
task_type="weekly",
|
||||
task_type=TaskType.WEEKLY,
|
||||
weekly_interval=1,
|
||||
run_asap_after_missed=True,
|
||||
run_time_bit_weekdays=127,
|
||||
@@ -549,7 +552,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 1",
|
||||
task_type="monthly",
|
||||
task_type=TaskType.MONTHLY,
|
||||
random_task_delay="3M",
|
||||
task_repetition_interval="15M",
|
||||
task_repetition_duration="1D",
|
||||
@@ -597,7 +600,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 1",
|
||||
task_type="monthlydow",
|
||||
task_type=TaskType.MONTHLY_DOW,
|
||||
run_time_bit_weekdays=56,
|
||||
monthly_months_of_year=0x400,
|
||||
monthly_weeks_of_month=3,
|
||||
@@ -637,7 +640,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 2",
|
||||
task_type="runonce",
|
||||
task_type=TaskType.RUN_ONCE,
|
||||
run_time_date=djangotime.now() + djangotime.timedelta(hours=22),
|
||||
run_asap_after_missed=True,
|
||||
)
|
||||
@@ -672,7 +675,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 3",
|
||||
task_type="runonce",
|
||||
task_type=TaskType.RUN_ONCE,
|
||||
run_asap_after_missed=True,
|
||||
run_time_date=djangotime.datetime(2018, 6, 1, 23, 23, 23),
|
||||
)
|
||||
@@ -703,7 +706,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"autotasks.AutomatedTask",
|
||||
agent=agent,
|
||||
name="test task 4",
|
||||
task_type="checkfailure",
|
||||
task_type=TaskType.CHECK_FAILURE,
|
||||
assigned_check=check,
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
@@ -731,7 +734,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
task1 = AutomatedTask.objects.create(
|
||||
agent=agent,
|
||||
name="test task 5",
|
||||
task_type="manual",
|
||||
task_type=TaskType.MANUAL,
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
create_win_task_schedule(pk=task1.pk)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from tacticalrmm.permissions import _has_perm_on_agent
|
||||
|
||||
from .models import AutomatedTask
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
from model_bakery.recipe import Recipe
|
||||
|
||||
from tacticalrmm.constants import CheckType, EvtLogTypes
|
||||
|
||||
check = Recipe("checks.Check")
|
||||
|
||||
diskspace_check = check.extend(
|
||||
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10
|
||||
check_type=CheckType.DISK_SPACE, disk="C:", warning_threshold=30, error_threshold=10
|
||||
)
|
||||
|
||||
cpuload_check = check.extend(
|
||||
check_type="cpuload", warning_threshold=30, error_threshold=75
|
||||
check_type=CheckType.CPU_LOAD, warning_threshold=30, error_threshold=75
|
||||
)
|
||||
|
||||
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
|
||||
|
||||
memory_check = check.extend(
|
||||
check_type="memory", warning_threshold=60, error_threshold=75
|
||||
check_type=CheckType.MEMORY, warning_threshold=60, error_threshold=75
|
||||
)
|
||||
|
||||
winsvc_check = check.extend(
|
||||
check_type="winsvc",
|
||||
check_type=CheckType.WINSVC,
|
||||
svc_name="ServiceName",
|
||||
svc_display_name="ServiceName",
|
||||
svc_policy_mode="manual",
|
||||
@@ -25,9 +27,9 @@ winsvc_check = check.extend(
|
||||
)
|
||||
|
||||
eventlog_check = check.extend(
|
||||
check_type="eventlog", event_id=5000, event_type="application"
|
||||
check_type=CheckType.EVENT_LOG, event_id=5000, event_type=EvtLogTypes.INFO
|
||||
)
|
||||
|
||||
script_check = check.extend(
|
||||
name="Script Name", check_type="script", script__name="Script Name"
|
||||
name="Script Name", check_type=CheckType.SCRIPT, script__name="Script Name"
|
||||
)
|
||||
|
||||
26
api/tacticalrmm/checks/constants.py
Normal file
26
api/tacticalrmm/checks/constants.py
Normal file
@@ -0,0 +1,26 @@
|
||||
CHECK_DEFER = (
|
||||
"created_by",
|
||||
"created_time",
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"timeout",
|
||||
"svc_display_name",
|
||||
"svc_policy_mode",
|
||||
"log_name",
|
||||
"event_id",
|
||||
"event_id_is_wildcard",
|
||||
"event_type",
|
||||
"event_source",
|
||||
"event_message",
|
||||
"fail_when",
|
||||
"search_last_days",
|
||||
)
|
||||
|
||||
CHECK_RESULT_DEFER = (
|
||||
"more_info",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"execution_time",
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
# Generated by Django 3.2.12 on 2022-04-01 22:44
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from django.db import migrations, transaction
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from tacticalrmm.constants import CheckType
|
||||
|
||||
|
||||
def migrate_check_results(apps, schema_editor):
|
||||
Check = apps.get_model("checks", "Check")
|
||||
@@ -28,7 +30,12 @@ def migrate_check_results(apps, schema_editor):
|
||||
history=check.history,
|
||||
alert_severity=check.alert_severity
|
||||
if check.check_type
|
||||
in ["cpuload", "memory", "diskspace", "script"]
|
||||
in [
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
else None,
|
||||
)
|
||||
|
||||
@@ -49,7 +56,12 @@ def migrate_check_results(apps, schema_editor):
|
||||
history=check.history,
|
||||
alert_severity=check.alert_severity
|
||||
if check.check_type
|
||||
in ["cpuload", "memory", "diskspace", "script"]
|
||||
in [
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]
|
||||
else None,
|
||||
)
|
||||
except IntegrityError:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-29 07:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0029_alter_checkresult_alert_severity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='checkresult',
|
||||
name='retcode',
|
||||
field=models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,59 +1,31 @@
|
||||
from statistics import mean
|
||||
from typing import TYPE_CHECKING, Any, Union, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
|
||||
from django.core.cache import cache
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
from core.utils import get_core_settings
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.constants import (
|
||||
CHECKS_NON_EDITABLE_FIELDS,
|
||||
POLICY_CHECK_FIELDS_TO_COPY,
|
||||
AlertSeverity,
|
||||
CheckStatus,
|
||||
CheckType,
|
||||
EvtLogFailWhen,
|
||||
EvtLogNames,
|
||||
EvtLogTypes,
|
||||
)
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.models import Agent # pragma: no cover
|
||||
from alerts.models import Alert, AlertTemplate # pragma: no cover
|
||||
from automation.models import Policy # pragma: no cover
|
||||
|
||||
CHECK_TYPE_CHOICES = [
|
||||
("diskspace", "Disk Space Check"),
|
||||
("ping", "Ping Check"),
|
||||
("cpuload", "CPU Load Check"),
|
||||
("memory", "Memory Check"),
|
||||
("winsvc", "Service Check"),
|
||||
("script", "Script Check"),
|
||||
("eventlog", "Event Log Check"),
|
||||
]
|
||||
|
||||
CHECK_STATUS_CHOICES = [
|
||||
("passing", "Passing"),
|
||||
("failing", "Failing"),
|
||||
("pending", "Pending"),
|
||||
]
|
||||
|
||||
EVT_LOG_NAME_CHOICES = [
|
||||
("Application", "Application"),
|
||||
("System", "System"),
|
||||
("Security", "Security"),
|
||||
]
|
||||
|
||||
EVT_LOG_TYPE_CHOICES = [
|
||||
("INFO", "Information"),
|
||||
("WARNING", "Warning"),
|
||||
("ERROR", "Error"),
|
||||
("AUDIT_SUCCESS", "Success Audit"),
|
||||
("AUDIT_FAILURE", "Failure Audit"),
|
||||
]
|
||||
|
||||
EVT_LOG_FAIL_WHEN_CHOICES = [
|
||||
("contains", "Log contains"),
|
||||
("not_contains", "Log does not contain"),
|
||||
]
|
||||
|
||||
|
||||
class Check(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
@@ -77,7 +49,7 @@ class Check(BaseAuditModel):
|
||||
overridden_by_policy = models.BooleanField(default=False)
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
check_type = models.CharField(
|
||||
max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace"
|
||||
max_length=50, choices=CheckType.choices, default=CheckType.DISK_SPACE
|
||||
)
|
||||
email_alert = models.BooleanField(default=False)
|
||||
text_alert = models.BooleanField(default=False)
|
||||
@@ -89,8 +61,8 @@ class Check(BaseAuditModel):
|
||||
# for eventlog, script, ip, and service alert severity
|
||||
alert_severity = models.CharField(
|
||||
max_length=15,
|
||||
choices=SEVERITY_CHOICES,
|
||||
default="warning",
|
||||
choices=AlertSeverity.choices,
|
||||
default=AlertSeverity.WARNING,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
@@ -151,17 +123,17 @@ class Check(BaseAuditModel):
|
||||
|
||||
# event log checks
|
||||
log_name = models.CharField(
|
||||
max_length=255, choices=EVT_LOG_NAME_CHOICES, null=True, blank=True
|
||||
max_length=255, choices=EvtLogNames.choices, null=True, blank=True
|
||||
)
|
||||
event_id = models.IntegerField(null=True, blank=True)
|
||||
event_id_is_wildcard = models.BooleanField(default=False)
|
||||
event_type = models.CharField(
|
||||
max_length=255, choices=EVT_LOG_TYPE_CHOICES, null=True, blank=True
|
||||
max_length=255, choices=EvtLogTypes.choices, null=True, blank=True
|
||||
)
|
||||
event_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
event_message = models.TextField(null=True, blank=True)
|
||||
fail_when = models.CharField(
|
||||
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
|
||||
max_length=255, choices=EvtLogFailWhen.choices, null=True, blank=True
|
||||
)
|
||||
search_last_days = models.PositiveIntegerField(null=True, blank=True)
|
||||
number_of_events_b4_alert = models.PositiveIntegerField(
|
||||
@@ -215,7 +187,7 @@ class Check(BaseAuditModel):
|
||||
@property
|
||||
def readable_desc(self):
|
||||
display = self.get_check_type_display() # type: ignore
|
||||
if self.check_type == "diskspace":
|
||||
if self.check_type == CheckType.DISK_SPACE:
|
||||
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
@@ -224,9 +196,11 @@ class Check(BaseAuditModel):
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
return f"{display}: Drive {self.disk} - {text}"
|
||||
elif self.check_type == "ping":
|
||||
elif self.check_type == CheckType.PING:
|
||||
return f"{display}: {self.name}"
|
||||
elif self.check_type == "cpuload" or self.check_type == "memory":
|
||||
elif (
|
||||
self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
|
||||
):
|
||||
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
@@ -235,11 +209,11 @@ class Check(BaseAuditModel):
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
return f"{display} - {text}"
|
||||
elif self.check_type == "winsvc":
|
||||
elif self.check_type == CheckType.WINSVC:
|
||||
return f"{display}: {self.svc_display_name}"
|
||||
elif self.check_type == "eventlog":
|
||||
elif self.check_type == CheckType.EVENT_LOG:
|
||||
return f"{display}: {self.name}"
|
||||
elif self.check_type == "script":
|
||||
elif self.check_type == CheckType.SCRIPT:
|
||||
return f"{display}: {self.script.name}"
|
||||
else:
|
||||
return "n/a"
|
||||
@@ -295,25 +269,22 @@ class Check(BaseAuditModel):
|
||||
return CheckAuditSerializer(check).data
|
||||
|
||||
def is_duplicate(self, check):
|
||||
if self.check_type == "diskspace":
|
||||
if self.check_type == CheckType.DISK_SPACE:
|
||||
return self.disk == check.disk
|
||||
|
||||
elif self.check_type == "script":
|
||||
elif self.check_type == CheckType.SCRIPT:
|
||||
return self.script == check.script
|
||||
|
||||
elif self.check_type == "ping":
|
||||
elif self.check_type == CheckType.PING:
|
||||
return self.ip == check.ip
|
||||
|
||||
elif self.check_type == "cpuload":
|
||||
elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
return True
|
||||
|
||||
elif self.check_type == "memory":
|
||||
return True
|
||||
|
||||
elif self.check_type == "winsvc":
|
||||
elif self.check_type == CheckType.WINSVC:
|
||||
return self.svc_name == check.svc_name
|
||||
|
||||
elif self.check_type == "eventlog":
|
||||
elif self.check_type == CheckType.EVENT_LOG:
|
||||
return [self.log_name, self.event_id] == [check.log_name, check.event_id]
|
||||
|
||||
|
||||
@@ -335,12 +306,12 @@ class CheckResult(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=100, choices=CHECK_STATUS_CHOICES, default="pending"
|
||||
max_length=100, choices=CheckStatus.choices, default=CheckStatus.PENDING
|
||||
)
|
||||
# for memory, diskspace, script, and cpu checks where severity changes
|
||||
alert_severity = models.CharField(
|
||||
max_length=15,
|
||||
choices=SEVERITY_CHOICES,
|
||||
choices=AlertSeverity.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
@@ -351,7 +322,7 @@ class CheckResult(models.Model):
|
||||
extra_details = models.JSONField(null=True, blank=True)
|
||||
stdout = models.TextField(null=True, blank=True)
|
||||
stderr = models.TextField(null=True, blank=True)
|
||||
retcode = models.IntegerField(null=True, blank=True)
|
||||
retcode = models.BigIntegerField(null=True, blank=True)
|
||||
execution_time = models.CharField(max_length=100, null=True, blank=True)
|
||||
# cpu and mem check history
|
||||
history = ArrayField(
|
||||
@@ -365,12 +336,12 @@ class CheckResult(models.Model):
|
||||
|
||||
# if check is a policy check clear cache on everything
|
||||
if not self.alert_severity and self.assigned_check.check_type in [
|
||||
"cpuload",
|
||||
"memory",
|
||||
"diskspace",
|
||||
"script",
|
||||
CheckType.MEMORY,
|
||||
CheckType.CPU_LOAD,
|
||||
CheckType.DISK_SPACE,
|
||||
CheckType.SCRIPT,
|
||||
]:
|
||||
self.alert_severity = "warning"
|
||||
self.alert_severity = AlertSeverity.WARNING
|
||||
|
||||
super(CheckResult, self).save(
|
||||
*args,
|
||||
@@ -379,10 +350,7 @@ class CheckResult(models.Model):
|
||||
|
||||
@property
|
||||
def history_info(self):
|
||||
if (
|
||||
self.assigned_check.check_type == "cpuload"
|
||||
or self.assigned_check.check_type == "memory"
|
||||
):
|
||||
if self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
return ", ".join(str(f"{x}%") for x in self.history[-6:])
|
||||
|
||||
def get_or_create_alert_if_needed(
|
||||
@@ -397,87 +365,86 @@ class CheckResult(models.Model):
|
||||
skip_create=not self.assigned_check.should_create_alert(alert_template),
|
||||
)
|
||||
|
||||
def handle_check(self, data):
|
||||
def handle_check(self, data, check: "Check", agent: "Agent"):
|
||||
from alerts.models import Alert
|
||||
|
||||
check = self.assigned_check
|
||||
|
||||
update_fields = []
|
||||
# cpuload or mem checks
|
||||
if check.check_type == "cpuload" or check.check_type == "memory":
|
||||
if check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
|
||||
self.history.append(data["percent"])
|
||||
|
||||
if len(self.history) > 15:
|
||||
self.history = self.history[-15:]
|
||||
|
||||
self.save(update_fields=["history"])
|
||||
update_fields.extend(["history"])
|
||||
|
||||
avg = int(mean(self.history))
|
||||
|
||||
if check.error_threshold and avg > check.error_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.ERROR
|
||||
elif check.warning_threshold and avg > check.warning_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "warning"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.WARNING
|
||||
else:
|
||||
self.status = "passing"
|
||||
self.status = CheckStatus.PASSING
|
||||
|
||||
# add check history
|
||||
check.add_check_history(data["percent"], self.agent.agent_id)
|
||||
check.add_check_history(data["percent"], agent.agent_id)
|
||||
|
||||
# diskspace checks
|
||||
elif check.check_type == "diskspace":
|
||||
elif check.check_type == CheckType.DISK_SPACE:
|
||||
if data["exists"]:
|
||||
percent_used = round(data["percent_used"])
|
||||
if (
|
||||
check.error_threshold
|
||||
and (100 - percent_used) < check.error_threshold
|
||||
):
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.ERROR
|
||||
elif (
|
||||
check.warning_threshold
|
||||
and (100 - percent_used) < check.warning_threshold
|
||||
):
|
||||
self.status = "failing"
|
||||
self.alert_severity = "warning"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.WARNING
|
||||
|
||||
else:
|
||||
self.status = "passing"
|
||||
self.status = CheckStatus.PASSING
|
||||
|
||||
self.more_info = data["more_info"]
|
||||
|
||||
# add check history
|
||||
check.add_check_history(100 - percent_used, self.agent.agent_id)
|
||||
check.add_check_history(100 - percent_used, agent.agent_id)
|
||||
else:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.ERROR
|
||||
self.more_info = f"Disk {check.disk} does not exist"
|
||||
|
||||
self.save(update_fields=["more_info"])
|
||||
update_fields.extend(["more_info"])
|
||||
|
||||
# script checks
|
||||
elif check.check_type == "script":
|
||||
elif check.check_type == CheckType.SCRIPT:
|
||||
self.stdout = data["stdout"]
|
||||
self.stderr = data["stderr"]
|
||||
self.retcode = data["retcode"]
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
|
||||
if data["retcode"] in check.info_return_codes:
|
||||
self.alert_severity = "info"
|
||||
self.status = "failing"
|
||||
self.alert_severity = AlertSeverity.INFO
|
||||
self.status = CheckStatus.FAILING
|
||||
elif data["retcode"] in check.warning_return_codes:
|
||||
self.alert_severity = "warning"
|
||||
self.status = "failing"
|
||||
self.alert_severity = AlertSeverity.WARNING
|
||||
self.status = CheckStatus.FAILING
|
||||
elif data["retcode"] != 0:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
self.status = CheckStatus.FAILING
|
||||
self.alert_severity = AlertSeverity.ERROR
|
||||
else:
|
||||
self.status = "passing"
|
||||
self.status = CheckStatus.PASSING
|
||||
|
||||
self.save(
|
||||
update_fields=[
|
||||
update_fields.extend(
|
||||
[
|
||||
"stdout",
|
||||
"stderr",
|
||||
"retcode",
|
||||
@@ -487,8 +454,8 @@ class CheckResult(models.Model):
|
||||
|
||||
# add check history
|
||||
check.add_check_history(
|
||||
1 if self.status == "failing" else 0,
|
||||
self.agent.agent_id,
|
||||
1 if self.status == CheckStatus.FAILING else 0,
|
||||
agent.agent_id,
|
||||
{
|
||||
"retcode": data["retcode"],
|
||||
"stdout": data["stdout"][:60],
|
||||
@@ -498,67 +465,73 @@ class CheckResult(models.Model):
|
||||
)
|
||||
|
||||
# ping checks
|
||||
elif check.check_type == "ping":
|
||||
elif check.check_type == CheckType.PING:
|
||||
self.status = data["status"]
|
||||
self.more_info = data["output"]
|
||||
self.save(update_fields=["more_info"])
|
||||
update_fields.extend(["more_info"])
|
||||
|
||||
check.add_check_history(
|
||||
1 if self.status == "failing" else 0,
|
||||
self.agent.agent_id,
|
||||
1 if self.status == CheckStatus.FAILING else 0,
|
||||
agent.agent_id,
|
||||
self.more_info[:60],
|
||||
)
|
||||
|
||||
# windows service checks
|
||||
elif check.check_type == "winsvc":
|
||||
elif check.check_type == CheckType.WINSVC:
|
||||
self.status = data["status"]
|
||||
self.more_info = data["more_info"]
|
||||
self.save(update_fields=["more_info"])
|
||||
update_fields.extend(["more_info"])
|
||||
|
||||
check.add_check_history(
|
||||
1 if self.status == "failing" else 0,
|
||||
self.agent.agent_id,
|
||||
1 if self.status == CheckStatus.FAILING else 0,
|
||||
agent.agent_id,
|
||||
self.more_info[:60],
|
||||
)
|
||||
|
||||
elif check.check_type == "eventlog":
|
||||
elif check.check_type == CheckType.EVENT_LOG:
|
||||
log = data["log"]
|
||||
if check.fail_when == "contains":
|
||||
if check.fail_when == EvtLogFailWhen.CONTAINS:
|
||||
if log and len(log) >= check.number_of_events_b4_alert:
|
||||
self.status = "failing"
|
||||
self.status = CheckStatus.FAILING
|
||||
else:
|
||||
self.status = "passing"
|
||||
self.status = CheckStatus.PASSING
|
||||
|
||||
elif check.fail_when == "not_contains":
|
||||
elif check.fail_when == EvtLogFailWhen.NOT_CONTAINS:
|
||||
if log and len(log) >= check.number_of_events_b4_alert:
|
||||
self.status = "passing"
|
||||
self.status = CheckStatus.PASSING
|
||||
else:
|
||||
self.status = "failing"
|
||||
self.status = CheckStatus.FAILING
|
||||
|
||||
self.extra_details = {"log": log}
|
||||
self.save(update_fields=["extra_details"])
|
||||
update_fields.extend(["extra_details"])
|
||||
|
||||
check.add_check_history(
|
||||
1 if self.status == "failing" else 0,
|
||||
self.agent.agent_id,
|
||||
1 if self.status == CheckStatus.FAILING else 0,
|
||||
agent.agent_id,
|
||||
"Events Found:" + str(len(self.extra_details["log"])),
|
||||
)
|
||||
|
||||
self.last_run = djangotime.now()
|
||||
# handle status
|
||||
if self.status == "failing":
|
||||
if self.status == CheckStatus.FAILING:
|
||||
self.fail_count += 1
|
||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||
update_fields.extend(["status", "fail_count", "alert_severity", "last_run"])
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
if self.fail_count >= check.fails_b4_alert:
|
||||
Alert.handle_alert_failure(self)
|
||||
|
||||
elif self.status == "passing":
|
||||
elif self.status == CheckStatus.PASSING:
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||
update_fields.extend(["status", "fail_count", "alert_severity", "last_run"])
|
||||
self.save(update_fields=update_fields)
|
||||
if Alert.objects.filter(
|
||||
assigned_check=check, agent=self.agent, resolved=False
|
||||
assigned_check=check, agent=agent, resolved=False
|
||||
).exists():
|
||||
Alert.handle_alert_resolve(self)
|
||||
else:
|
||||
update_fields.extend(["last_run"])
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
return self.status
|
||||
|
||||
@@ -572,7 +545,7 @@ class CheckResult(models.Model):
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
if self.assigned_check.check_type == "diskspace":
|
||||
if self.assigned_check.check_type == CheckType.DISK_SPACE:
|
||||
text = ""
|
||||
if self.assigned_check.warning_threshold:
|
||||
text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
|
||||
@@ -591,21 +564,18 @@ class CheckResult(models.Model):
|
||||
except:
|
||||
body = subject + f" - Disk {self.assigned_check.disk} does not exist"
|
||||
|
||||
elif self.assigned_check.check_type == "script":
|
||||
elif self.assigned_check.check_type == CheckType.SCRIPT:
|
||||
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
elif self.assigned_check.check_type == "ping":
|
||||
elif self.assigned_check.check_type == CheckType.PING:
|
||||
|
||||
body = self.more_info
|
||||
|
||||
elif (
|
||||
self.assigned_check.check_type == "cpuload"
|
||||
or self.assigned_check.check_type == "memory"
|
||||
):
|
||||
elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
text = ""
|
||||
if self.assigned_check.warning_threshold:
|
||||
text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
|
||||
@@ -614,16 +584,16 @@ class CheckResult(models.Model):
|
||||
|
||||
avg = int(mean(self.history))
|
||||
|
||||
if self.assigned_check.check_type == "cpuload":
|
||||
if self.assigned_check.check_type == CheckType.CPU_LOAD:
|
||||
body = subject + f" - Average CPU utilization: {avg}%, {text}"
|
||||
|
||||
elif self.assigned_check.check_type == "memory":
|
||||
elif self.assigned_check.check_type == CheckType.MEMORY:
|
||||
body = subject + f" - Average memory usage: {avg}%, {text}"
|
||||
|
||||
elif self.assigned_check.check_type == "winsvc":
|
||||
elif self.assigned_check.check_type == CheckType.WINSVC:
|
||||
body = subject + f" - Status: {self.more_info}"
|
||||
|
||||
elif self.assigned_check.check_type == "eventlog":
|
||||
elif self.assigned_check.check_type == CheckType.EVENT_LOG:
|
||||
|
||||
if self.assigned_check.event_source and self.assigned_check.event_message:
|
||||
start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source}, containing string {self.assigned_check.event_message} "
|
||||
@@ -655,7 +625,7 @@ class CheckResult(models.Model):
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
if self.assigned_check.check_type == "diskspace":
|
||||
if self.assigned_check.check_type == CheckType.DISK_SPACE:
|
||||
text = ""
|
||||
if self.assigned_check.warning_threshold:
|
||||
text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
|
||||
@@ -673,14 +643,11 @@ class CheckResult(models.Model):
|
||||
except:
|
||||
body = subject + f" - Disk {self.assigned_check.disk} does not exist"
|
||||
|
||||
elif self.assigned_check.check_type == "script":
|
||||
elif self.assigned_check.check_type == CheckType.SCRIPT:
|
||||
body = subject + f" - Return code: {self.retcode}"
|
||||
elif self.assigned_check.check_type == "ping":
|
||||
elif self.assigned_check.check_type == CheckType.PING:
|
||||
body = subject
|
||||
elif (
|
||||
self.assigned_check.check_type == "cpuload"
|
||||
or self.assigned_check.check_type == "memory"
|
||||
):
|
||||
elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
|
||||
text = ""
|
||||
if self.assigned_check.warning_threshold:
|
||||
text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
|
||||
@@ -688,13 +655,13 @@ class CheckResult(models.Model):
|
||||
text += f" Error Threshold: {self.assigned_check.error_threshold}%"
|
||||
|
||||
avg = int(mean(self.history))
|
||||
if self.assigned_check.check_type == "cpuload":
|
||||
if self.assigned_check.check_type == CheckType.CPU_LOAD:
|
||||
body = subject + f" - Average CPU utilization: {avg}%, {text}"
|
||||
elif self.assigned_check.check_type == "memory":
|
||||
elif self.assigned_check.check_type == CheckType.MEMORY:
|
||||
body = subject + f" - Average memory usage: {avg}%, {text}"
|
||||
elif self.assigned_check.check_type == "winsvc":
|
||||
elif self.assigned_check.check_type == CheckType.WINSVC:
|
||||
body = subject + f" - Status: {self.more_info}"
|
||||
elif self.assigned_check.check_type == "eventlog":
|
||||
elif self.assigned_check.check_type == CheckType.EVENT_LOG:
|
||||
body = subject
|
||||
|
||||
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import validators as _v
|
||||
from autotasks.models import AutomatedTask
|
||||
from rest_framework import serializers
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from scripts.models import Script
|
||||
from scripts.serializers import ScriptCheckSerializer
|
||||
from tacticalrmm.constants import CheckType
|
||||
|
||||
from .models import Check, CheckHistory, CheckResult
|
||||
|
||||
@@ -67,9 +69,11 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
# disk checks
|
||||
# make sure no duplicate diskchecks exist for an agent/policy
|
||||
if check_type == "diskspace":
|
||||
if check_type == CheckType.DISK_SPACE:
|
||||
if not self.instance: # only on create
|
||||
checks = Check.objects.filter(**filter).filter(check_type="diskspace")
|
||||
checks = Check.objects.filter(**filter).filter(
|
||||
check_type=CheckType.DISK_SPACE
|
||||
)
|
||||
for check in checks:
|
||||
if val["disk"] in check.disk:
|
||||
raise serializers.ValidationError(
|
||||
@@ -91,7 +95,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
# ping checks
|
||||
if check_type == "ping":
|
||||
if check_type == CheckType.PING:
|
||||
if (
|
||||
not _v.ipv4(val["ip"])
|
||||
and not _v.ipv6(val["ip"])
|
||||
@@ -101,8 +105,8 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
"Please enter a valid IP address or domain name"
|
||||
)
|
||||
|
||||
if check_type == "cpuload" and not self.instance:
|
||||
if Check.objects.filter(**filter, check_type="cpuload").exists():
|
||||
if check_type == CheckType.CPU_LOAD and not self.instance:
|
||||
if Check.objects.filter(**filter, check_type=CheckType.CPU_LOAD).exists():
|
||||
raise serializers.ValidationError(
|
||||
"A cpuload check for this agent already exists"
|
||||
)
|
||||
@@ -121,8 +125,8 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
f"Warning threshold must be less than Error Threshold"
|
||||
)
|
||||
|
||||
if check_type == "memory" and not self.instance:
|
||||
if Check.objects.filter(**filter, check_type="memory").exists():
|
||||
if check_type == CheckType.MEMORY and not self.instance:
|
||||
if Check.objects.filter(**filter, check_type=CheckType.MEMORY).exists():
|
||||
raise serializers.ValidationError(
|
||||
"A memory check for this agent already exists"
|
||||
)
|
||||
@@ -156,11 +160,12 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
script_args = serializers.SerializerMethodField()
|
||||
|
||||
def get_script_args(self, obj):
|
||||
if obj.check_type != "script":
|
||||
if obj.check_type != CheckType.SCRIPT:
|
||||
return []
|
||||
|
||||
agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent
|
||||
return Script.parse_script_args(
|
||||
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
|
||||
agent=agent, shell=obj.script.shell, args=obj.script_args
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.utils import timezone as djangotime
|
||||
|
||||
from alerts.models import Alert
|
||||
from checks.models import CheckResult
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from checks.models import CheckHistory, CheckResult
|
||||
from django.conf import settings
|
||||
from django.test import modify_settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from checks.models import CheckHistory, CheckResult
|
||||
from tacticalrmm.constants import (
|
||||
AlertSeverity,
|
||||
CheckStatus,
|
||||
CheckType,
|
||||
EvtLogFailWhen,
|
||||
EvtLogTypes,
|
||||
)
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .serializers import CheckSerializer
|
||||
@@ -13,11 +19,6 @@ from .serializers import CheckSerializer
|
||||
base_url = "/checks"
|
||||
|
||||
|
||||
@modify_settings(
|
||||
MIDDLEWARE={
|
||||
"remove": "tacticalrmm.middleware.LinuxMiddleware",
|
||||
}
|
||||
)
|
||||
class TestCheckViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
@@ -84,7 +85,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"check_type": CheckType.DISK_SPACE,
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
@@ -93,7 +94,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "diskspace",
|
||||
"check_type": CheckType.DISK_SPACE,
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
@@ -133,7 +134,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "cpuload",
|
||||
"check_type": CheckType.CPU_LOAD,
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
@@ -141,7 +142,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "cpuload",
|
||||
"check_type": CheckType.CPU_LOAD,
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
@@ -180,7 +181,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "memory",
|
||||
"check_type": CheckType.MEMORY,
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
@@ -188,7 +189,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "memory",
|
||||
"check_type": CheckType.MEMORY,
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
@@ -347,8 +348,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test passing
|
||||
data = {
|
||||
@@ -365,7 +366,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
# test failing info
|
||||
check.info_return_codes = [20, 30, 50]
|
||||
@@ -385,8 +386,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "info")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.INFO)
|
||||
|
||||
# test failing warning
|
||||
check.warning_return_codes = [80, 100, 1040]
|
||||
@@ -406,8 +407,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
def test_handle_diskspace_check(self):
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -435,8 +436,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
# test error failure
|
||||
data = {
|
||||
@@ -454,8 +455,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test disk not exist
|
||||
data = {"id": check.id, "agent_id": self.agent.agent_id, "exists": False}
|
||||
@@ -465,8 +466,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test warning threshold 0
|
||||
check.warning_threshold = 0
|
||||
@@ -486,8 +487,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test error threshold 0
|
||||
check.warning_threshold = 50
|
||||
@@ -507,8 +508,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
# test passing
|
||||
data = {
|
||||
@@ -526,7 +527,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
def test_handle_cpuload_check(self):
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -545,8 +546,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
# test failing error
|
||||
data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95}
|
||||
@@ -559,8 +560,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test passing
|
||||
data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 50}
|
||||
@@ -573,7 +574,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
# test warning threshold 0
|
||||
check.warning_threshold = 0
|
||||
@@ -588,8 +589,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test error threshold 0
|
||||
check.warning_threshold = 50
|
||||
@@ -605,8 +606,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
def test_handle_memory_check(self):
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -625,8 +626,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
# test failing error
|
||||
data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95}
|
||||
@@ -639,8 +640,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test passing
|
||||
data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 50}
|
||||
@@ -653,7 +654,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
# test warning threshold 0
|
||||
check.warning_threshold = 0
|
||||
@@ -668,8 +669,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test error threshold 0
|
||||
check.warning_threshold = 50
|
||||
@@ -685,21 +686,21 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check_result.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
def test_handle_ping_check(self):
|
||||
url = "/api/v3/checkrunner/"
|
||||
|
||||
check = baker.make_recipe(
|
||||
"checks.ping_check", agent=self.agent, alert_severity="info"
|
||||
"checks.ping_check", agent=self.agent, alert_severity=AlertSeverity.INFO
|
||||
)
|
||||
|
||||
# test failing info
|
||||
data = {
|
||||
"id": check.id,
|
||||
"agent_id": self.agent.agent_id,
|
||||
"status": "failing",
|
||||
"status": CheckStatus.FAILING,
|
||||
"output": "reply from a.com",
|
||||
}
|
||||
|
||||
@@ -707,44 +708,44 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, "info")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.INFO)
|
||||
|
||||
# test failing warning
|
||||
check.alert_severity = "warning"
|
||||
check.alert_severity = AlertSeverity.WARNING
|
||||
check.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, "warning")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.WARNING)
|
||||
|
||||
# test failing error
|
||||
check.alert_severity = "error"
|
||||
check.alert_severity = AlertSeverity.ERROR
|
||||
check.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test failing error
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test passing
|
||||
data = {
|
||||
"id": check.id,
|
||||
"agent_id": self.agent.agent_id,
|
||||
"status": "passing",
|
||||
"status": CheckStatus.PASSING,
|
||||
"output": "reply from a.com",
|
||||
}
|
||||
|
||||
@@ -752,21 +753,21 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_handle_winsvc_check(self, nats_cmd):
|
||||
url = "/api/v3/checkrunner/"
|
||||
|
||||
check = baker.make_recipe(
|
||||
"checks.winsvc_check", agent=self.agent, alert_severity="info"
|
||||
"checks.winsvc_check", agent=self.agent, alert_severity=AlertSeverity.INFO
|
||||
)
|
||||
|
||||
# test passing running
|
||||
data = {
|
||||
"id": check.id,
|
||||
"agent_id": self.agent.agent_id,
|
||||
"status": "passing",
|
||||
"status": CheckStatus.PASSING,
|
||||
"more_info": "ok",
|
||||
}
|
||||
|
||||
@@ -774,13 +775,13 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
# test failing
|
||||
data = {
|
||||
"id": check.id,
|
||||
"agent_id": self.agent.agent_id,
|
||||
"status": "failing",
|
||||
"status": CheckStatus.FAILING,
|
||||
"more_info": "ok",
|
||||
}
|
||||
|
||||
@@ -788,18 +789,18 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
self.assertEqual(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, "info")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.INFO)
|
||||
|
||||
def test_handle_eventlog_check(self):
|
||||
url = "/api/v3/checkrunner/"
|
||||
|
||||
check = baker.make_recipe(
|
||||
"checks.eventlog_check",
|
||||
event_type="warning",
|
||||
fail_when="contains",
|
||||
event_type=EvtLogTypes.WARNING,
|
||||
fail_when=EvtLogFailWhen.CONTAINS,
|
||||
event_id=123,
|
||||
alert_severity="warning",
|
||||
alert_severity=AlertSeverity.WARNING,
|
||||
agent=self.agent,
|
||||
)
|
||||
|
||||
@@ -842,8 +843,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEquals(check.alert_severity, "warning")
|
||||
self.assertEquals(check_result.status, "failing")
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.WARNING)
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
|
||||
# test passing when contains
|
||||
resp = self.client.patch(url, no_logs_data, format="json")
|
||||
@@ -851,11 +852,11 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEquals(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
# test failing when not contains and message and source
|
||||
check.fail_when = "not_contains"
|
||||
check.alert_severity = "error"
|
||||
check.fail_when = EvtLogFailWhen.NOT_CONTAINS
|
||||
check.alert_severity = AlertSeverity.ERROR
|
||||
check.save()
|
||||
|
||||
resp = self.client.patch(url, no_logs_data, format="json")
|
||||
@@ -863,8 +864,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEquals(check_result.status, "failing")
|
||||
self.assertEquals(check.alert_severity, "error")
|
||||
self.assertEqual(check_result.status, CheckStatus.FAILING)
|
||||
self.assertEqual(check.alert_severity, AlertSeverity.ERROR)
|
||||
|
||||
# test passing when contains with source and message
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -872,7 +873,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
|
||||
|
||||
self.assertEquals(check_result.status, "passing")
|
||||
self.assertEqual(check_result.status, CheckStatus.PASSING)
|
||||
|
||||
|
||||
class TestCheckPermissions(TacticalTestCase):
|
||||
@@ -945,7 +946,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
|
||||
policy_data = {
|
||||
"policy": policy.id,
|
||||
"check_type": "diskspace",
|
||||
"check_type": CheckType.DISK_SPACE,
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
@@ -954,7 +955,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
|
||||
agent_data = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"check_type": CheckType.DISK_SPACE,
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
@@ -963,7 +964,7 @@ class TestCheckPermissions(TacticalTestCase):
|
||||
|
||||
unauthorized_agent_data = {
|
||||
"agent": unauthorized_agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"check_type": CheckType.DISK_SPACE,
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import asyncio
|
||||
from datetime import datetime as dt
|
||||
|
||||
from agents.models import Agent
|
||||
from alerts.models import Alert
|
||||
from automation.models import Policy
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
@@ -13,8 +10,12 @@ from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from alerts.models import Alert
|
||||
from automation.models import Policy
|
||||
from tacticalrmm.constants import CheckStatus, CheckType
|
||||
from tacticalrmm.helpers import notify_error
|
||||
from tacticalrmm.permissions import _has_perm_on_agent
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Check, CheckHistory, CheckResult
|
||||
from .permissions import ChecksPerms, RunChecksPerms
|
||||
@@ -48,7 +49,7 @@ class GetAddChecks(APIView):
|
||||
|
||||
# set event id to 0 if wildcard because it needs to be an integer field for db
|
||||
# will be ignored anyway by the agent when doing wildcard check
|
||||
if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
|
||||
if data["check_type"] == CheckType.EVENT_LOG and data["event_id_is_wildcard"]:
|
||||
data["event_id"] = 0
|
||||
|
||||
serializer = CheckSerializer(data=data, partial=True)
|
||||
@@ -81,7 +82,7 @@ class GetUpdateDeleteCheck(APIView):
|
||||
|
||||
# set event id to 0 if wildcard because it needs to be an integer field for db
|
||||
# will be ignored anyway by the agent when doing wildcard check
|
||||
if check.check_type == "eventlog":
|
||||
if check.check_type == CheckType.EVENT_LOG:
|
||||
try:
|
||||
data["event_id_is_wildcard"]
|
||||
except KeyError:
|
||||
@@ -116,7 +117,7 @@ class ResetCheck(APIView):
|
||||
if result.agent and not _has_perm_on_agent(request.user, result.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
result.status = "passing"
|
||||
result.status = CheckStatus.PASSING
|
||||
result.save()
|
||||
|
||||
# resolve any alerts that are open
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Generated by Django 3.2.10 on 2021-12-26 05:47
|
||||
|
||||
import clients.models
|
||||
from django.db import migrations, models
|
||||
|
||||
import clients.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db import migrations
|
||||
|
||||
from tacticalrmm.constants import GoArch
|
||||
|
||||
|
||||
def change_arch(apps, schema_editor):
|
||||
Deployment = apps.get_model("clients", "Deployment")
|
||||
for d in Deployment.objects.all():
|
||||
if d.arch == "64":
|
||||
d.arch = GoArch.AMD64
|
||||
else:
|
||||
d.arch = GoArch.i386
|
||||
|
||||
d.save(update_fields=["arch"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("clients", "0021_remove_client_agent_count_remove_site_agent_count"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(change_arch),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-19 07:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("clients", "0022_change_arch_to_goarch"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="deployment",
|
||||
old_name="arch",
|
||||
new_name="goarch",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="deployment",
|
||||
name="goarch",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("amd64", "amd64"),
|
||||
("386", "386"),
|
||||
("arm64", "arm64"),
|
||||
("arm", "arm"),
|
||||
],
|
||||
default="amd64",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-23 05:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0023_alter_deployment_arch_deployment_goarch'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='deployment',
|
||||
name='goarch',
|
||||
field=models.CharField(choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], default='amd64', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,12 +1,13 @@
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
|
||||
from agents.models import Agent
|
||||
from django.core.cache import cache
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from typing import Dict
|
||||
from tacticalrmm.constants import AGENT_DEFER
|
||||
from tacticalrmm.constants import AGENT_DEFER, AgentMonType, CustomFieldType, GoArch
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
|
||||
@@ -168,17 +169,6 @@ class Site(BaseAuditModel):
|
||||
return SiteAuditSerializer(site).data
|
||||
|
||||
|
||||
MON_TYPE_CHOICES = [
|
||||
("server", "Server"),
|
||||
("workstation", "Workstation"),
|
||||
]
|
||||
|
||||
ARCH_CHOICES = [
|
||||
("64", "64 bit"),
|
||||
("32", "32 bit"),
|
||||
]
|
||||
|
||||
|
||||
class Deployment(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
@@ -187,9 +177,11 @@ class Deployment(models.Model):
|
||||
"clients.Site", related_name="deploysites", on_delete=models.CASCADE
|
||||
)
|
||||
mon_type = models.CharField(
|
||||
max_length=255, choices=MON_TYPE_CHOICES, default="server"
|
||||
max_length=255, choices=AgentMonType.choices, default=AgentMonType.SERVER
|
||||
)
|
||||
goarch = models.CharField(
|
||||
max_length=255, choices=GoArch.choices, default=GoArch.AMD64
|
||||
)
|
||||
arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
|
||||
expiry = models.DateTimeField(null=True, blank=True)
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
auth_token = models.ForeignKey(
|
||||
@@ -233,26 +225,26 @@ class ClientCustomField(models.Model):
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if self.field.type == "multiple":
|
||||
if self.field.type == CustomFieldType.MULTIPLE:
|
||||
return self.multiple_value
|
||||
elif self.field.type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return self.string_value
|
||||
|
||||
def save_to_field(self, value):
|
||||
if self.field.type in [
|
||||
"text",
|
||||
"number",
|
||||
"single",
|
||||
"datetime",
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
self.string_value = value
|
||||
self.save()
|
||||
elif type == "multiple":
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
self.multiple_value = value.split(",")
|
||||
self.save()
|
||||
elif type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
self.bool_value = bool(value)
|
||||
self.save()
|
||||
|
||||
@@ -284,25 +276,25 @@ class SiteCustomField(models.Model):
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if self.field.type == "multiple":
|
||||
if self.field.type == CustomFieldType.MULTIPLE:
|
||||
return self.multiple_value
|
||||
elif self.field.type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
return self.bool_value
|
||||
else:
|
||||
return self.string_value
|
||||
|
||||
def save_to_field(self, value):
|
||||
if self.field.type in [
|
||||
"text",
|
||||
"number",
|
||||
"single",
|
||||
"datetime",
|
||||
CustomFieldType.TEXT,
|
||||
CustomFieldType.NUMBER,
|
||||
CustomFieldType.SINGLE,
|
||||
CustomFieldType.DATETIME,
|
||||
]:
|
||||
self.string_value = value
|
||||
self.save()
|
||||
elif type == "multiple":
|
||||
elif self.field.type == CustomFieldType.MULTIPLE:
|
||||
self.multiple_value = value.split(",")
|
||||
self.save()
|
||||
elif type == "checkbox":
|
||||
elif self.field.type == CustomFieldType.CHECKBOX:
|
||||
self.bool_value = bool(value)
|
||||
self.save()
|
||||
|
||||
@@ -141,7 +141,7 @@ class DeploymentSerializer(ModelSerializer):
|
||||
"client_name",
|
||||
"site_name",
|
||||
"mon_type",
|
||||
"arch",
|
||||
"goarch",
|
||||
"expiry",
|
||||
"install_flags",
|
||||
"created",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user