Compare commits

...

175 Commits

Author SHA1 Message Date
wh1te909
48375f3878 Release 0.4.23 2021-03-11 00:35:02 +00:00
wh1te909
176c85d8c1 bump versions 2021-03-11 00:32:31 +00:00
wh1te909
17cad71ede typo 2021-03-10 22:46:11 +00:00
wh1te909
e8bf9d4e6f change thresholds for check run interval 2021-03-10 22:39:16 +00:00
wh1te909
7bdd2038ef enable django admin during install so that it installs properly, disable it at end of install 2021-03-10 22:32:36 +00:00
wh1te909
e9f6e7943a bump mesh 2021-03-10 19:52:37 +00:00
wh1te909
e74ba387ab update reqs 2021-03-10 19:03:11 +00:00
wh1te909
27c79e5b99 refactor method 2021-03-09 09:39:58 +00:00
wh1te909
8170d5ea73 feat: add client tree sorting closes #316 2021-03-09 03:17:43 +00:00
wh1te909
196f73705d isort 2021-03-09 03:14:56 +00:00
wh1te909
ad0bbf5248 add sorting back to status closes #305 2021-03-08 21:17:26 +00:00
wh1te909
4cae9cd90d add hostname to email subject 2021-03-08 06:58:02 +00:00
wh1te909
be7bc55a76 remove redundant buttons that are already in context menus 2021-03-07 10:21:46 +00:00
wh1te909
684b545e8f exclude date 2021-03-07 10:21:08 +00:00
wh1te909
7835cc3b10 update community scripts 2021-03-06 22:11:58 +00:00
Tragic Bronson
f8706b51e8 Merge pull request #314 from nr-plaxon/patch-3
Adding script to create an all-user logon script
2021-03-06 13:56:32 -08:00
nr-plaxon
d97f8fd5da Adding script to create an all-user logon script 2021-03-06 14:40:53 +01:00
sadnub
f8fa87441e black 2021-03-05 23:32:40 -05:00
sadnub
d42537814a sort of addresses #177. Allow ability to override check intervals 2021-03-05 23:27:54 -05:00
sadnub
792421b0e2 adds #66. EventLog Check: Set the number of event logs found before passing/failing 2021-03-05 21:52:08 -05:00
wh1te909
72d55a010b Release 0.4.22 2021-03-05 23:05:17 +00:00
wh1te909
880d8258ce bump versions 2021-03-05 23:02:08 +00:00
wh1te909
b79bf82efb update docs 2021-03-05 22:22:49 +00:00
wh1te909
b3118b6253 add fields to queryset 2021-03-05 09:30:53 +00:00
sadnub
ba172e2e25 fix issue with exception when other pending actions types exists 2021-03-04 16:31:25 -05:00
sadnub
892d53abeb move alert_template to property on agent versus dynamically generating it everytime 2021-03-04 16:27:05 -05:00
sadnub
5cbaa1ce98 fix tests 2021-03-03 22:25:02 -05:00
sadnub
7b35d9ad2e add policy sync to automation manager 2021-03-03 22:03:11 -05:00
wh1te909
8462de7911 fix wording 2021-03-04 02:20:54 +00:00
wh1te909
8721f44298 fix tests 2021-03-04 01:10:52 +00:00
wh1te909
c7a2d69afa rework agent recovery wh1te909/rmmagent@cef1a0efed 2021-03-04 00:51:03 +00:00
wh1te909
0453d81e7a fix pendingactions count 2021-03-03 11:07:20 +00:00
wh1te909
501c04ac2b Release 0.4.21 2021-03-03 10:44:49 +00:00
wh1te909
0ef4e9a5c3 bump versions 2021-03-03 10:44:34 +00:00
wh1te909
129c50e598 fix search/sort 2021-03-03 10:17:45 +00:00
wh1te909
3e276fc2ac isort 2021-03-03 10:17:06 +00:00
sadnub
658d5e05ae black 2021-03-02 23:38:13 -05:00
sadnub
4e7d5d476e add policy exclusions 2021-03-02 23:33:34 -05:00
wh1te909
6a55ca20f3 Release 0.4.20 2021-03-02 23:42:38 +00:00
wh1te909
c56c537f7f HOTFIX 0.4.20 temporarily disable some sorting 2021-03-02 23:42:00 +00:00
wh1te909
fd7d776121 Release 0.4.19 2021-03-02 22:18:18 +00:00
wh1te909
1af28190d8 bump versions 2021-03-02 22:11:40 +00:00
wh1te909
6b305be567 add dash 2021-03-02 22:08:15 +00:00
wh1te909
3bf70513b7 isort 2021-03-02 09:18:35 +00:00
wh1te909
7e64404654 add type hints 2021-03-02 09:13:24 +00:00
wh1te909
e1b5226f34 fix alert 2021-03-02 08:46:41 +00:00
wh1te909
0d7128ad31 Revert "bump versions"
This reverts commit 5778626087.
2021-03-02 08:41:17 +00:00
wh1te909
5778626087 bump versions 2021-03-02 08:07:39 +00:00
wh1te909
3ff48756ed continue on defender errors 2021-03-02 07:38:14 +00:00
sadnub
0ce9a6eeba black 2021-03-01 22:14:48 -05:00
sadnub
ad527b4aed alerts rework and tests 2021-03-01 22:10:38 -05:00
sadnub
6633bb452e remove jest and add cypress for frontend testing 2021-03-01 22:10:38 -05:00
wh1te909
efeb0b4feb add tests 2021-03-02 00:45:37 +00:00
wh1te909
8cc11fc102 fix pendingactions ui 2021-03-02 00:39:42 +00:00
Tragic Bronson
ee6a167220 Merge pull request #302 from silversword411/patch-2
tweak for workflow
2021-03-01 16:16:38 -08:00
silversword411
8d4ad3c405 tweak for workflow 2021-03-01 19:11:01 -05:00
Tragic Bronson
072fbf4d60 Merge pull request #299 from silversword411/patch-3
Linking to FAQ
2021-03-01 15:24:55 -08:00
silversword411
727c41c283 Update install_server.md 2021-03-01 18:15:12 -05:00
silversword411
e2266838b6 Linking to FAQ
minor update and link to FAQ
2021-03-01 17:59:53 -05:00
Tragic Bronson
775762d615 Merge pull request #298 from silversword411/patch-2
Fixing bash commands
2021-03-01 14:56:34 -08:00
silversword411
900c3008cb Fixing bash commands
Removing ID/server so paste will work
2021-03-01 17:44:35 -05:00
sadnub
09379213a6 fix formatting 2021-03-01 17:37:24 -05:00
sadnub
ceb97048e3 Update mkdocs.yml 2021-03-01 17:34:27 -05:00
sadnub
4561515517 Create update_docker.md 2021-03-01 17:33:41 -05:00
wh1te909
a7b285759f delete chocolog model 2021-03-01 21:43:54 +00:00
wh1te909
b4531b2a12 ui tweaks 2021-03-01 21:37:59 +00:00
wh1te909
9e1d261c76 update faq 2021-03-01 21:09:12 +00:00
Tragic Bronson
e35fa15cd2 Merge pull request #297 from silversword411/patch-1
Docs addition - Recover login for Mesh Central
2021-03-01 13:01:11 -08:00
wh1te909
dbd1f0d4f9 pending actions refactor 2021-03-01 20:40:46 +00:00
wh1te909
9ade78b703 fix restore docs 2021-03-01 19:45:10 +00:00
silversword411
f20e244b5f Recover login for Mesh Central 2021-03-01 12:50:56 -05:00
wh1te909
0989308b7e fix tests 2021-03-01 09:35:26 +00:00
wh1te909
12c7140536 more choco rework 2021-03-01 09:26:37 +00:00
wh1te909
2a0b605e92 return empty val for missing software install date 2021-03-01 08:21:56 +00:00
wh1te909
6978890e6a add contributing docs 2021-03-01 07:51:31 +00:00
Tragic Bronson
561abd6cb9 Merge pull request #296 from beejayzed/develop
Add community script to verify antivirus status
2021-02-28 23:32:55 -08:00
beejayzed
4dd6227f0b Update community_scripts.json 2021-03-01 07:55:31 +07:00
beejayzed
1ec314c31c Rename VerifyAntivirus to VerifyAntivirus.ps1 2021-03-01 07:52:43 +07:00
beejayzed
a2be5a00be Create VerifyAntivirus 2021-03-01 07:50:56 +07:00
wh1te909
4e2241c115 start chocolatey rework 2021-02-28 11:00:45 +00:00
wh1te909
8459bca64a fix nats ping dict 2021-02-28 09:54:53 +00:00
wh1te909
24cb0565b9 add pagination to agent table 2021-02-28 09:18:04 +00:00
wh1te909
9442acb028 fix pipeline typo 2021-02-27 23:41:08 +00:00
wh1te909
4f7f181a42 fix pipeline 2021-02-27 23:24:06 +00:00
wh1te909
b7dd8737a7 make django admin disabled by default 2021-02-27 23:19:35 +00:00
wh1te909
2207eeb727 add missing import 2021-02-27 23:09:01 +00:00
wh1te909
89dad7dfe7 add sponsors info to docs 2021-02-27 22:37:11 +00:00
wh1te909
e5803d0cf3 bump mesh 2021-02-27 07:45:56 +00:00
wh1te909
c1fffe9ae6 add timeout to net 2021-02-27 06:08:42 +00:00
wh1te909
9e6cbd3d32 set uwsgi procs based on cpu count 2021-02-27 05:28:42 +00:00
wh1te909
2ea8742510 natsapi refactor 2021-02-27 00:23:03 +00:00
wh1te909
5cfa0254f9 isort 2021-02-26 23:25:44 +00:00
wh1te909
8cd2544f78 add new management command 2021-02-26 22:05:42 +00:00
wh1te909
c03b768364 fix typos 2021-02-26 09:01:14 +00:00
wh1te909
d60481ead4 add docs for management commands 2021-02-25 20:55:56 +00:00
Tragic Bronson
126be3827d Merge pull request #292 from bradhawkins85/patch-6
Update installer.ps1
2021-02-25 10:06:04 -08:00
bradhawkins85
121274dca2 Update installer.ps1
Don't try and add Windows Defender exceptions if Defender is not enabled, prevents errors during script execution.
2021-02-25 19:59:29 +10:00
wh1te909
0ecf8da27e add management commands for resetting pw/2fa 2021-02-25 07:56:17 +00:00
wh1te909
4a6bcb525d update docs 2021-02-25 07:55:13 +00:00
wh1te909
83f9ee50dd add management commands for resetting pw/2fa 2021-02-25 07:55:03 +00:00
wh1te909
2bff297f79 Release 0.4.18 2021-02-24 20:52:49 +00:00
wh1te909
dee68f6933 bump versions 2021-02-24 20:51:47 +00:00
wh1te909
afa1e19c83 also grep postgres info during restore #285 2021-02-24 20:39:02 +00:00
wh1te909
6052088eb4 grab postgres creds automatically for backup closes #285 2021-02-24 19:23:47 +00:00
wh1te909
c7fa5167c4 also reinstall py env / node modules during forced update 2021-02-24 11:25:42 +00:00
wh1te909
1034b0b146 also reinstall py env / node modules during forced update 2021-02-24 11:24:47 +00:00
wh1te909
8bcc4e5945 fix docs styling 2021-02-24 10:04:45 +00:00
wh1te909
c3c24aa1db black 2021-02-24 09:46:38 +00:00
wh1te909
281c75d2d2 add find_software management command 2021-02-24 09:42:24 +00:00
wh1te909
52307420f3 more docs 2021-02-24 09:36:59 +00:00
wh1te909
6185347cd8 remove border 2021-02-24 09:34:30 +00:00
wh1te909
b6cd29f77e change wording 2021-02-24 09:26:36 +00:00
wh1te909
b8ea8b1567 typo 2021-02-24 08:38:44 +00:00
wh1te909
2f7dc98830 change save query 2021-02-24 07:37:48 +00:00
wh1te909
e248a99f79 add option to run sched task asap after scheduled start was missed #247 2021-02-24 06:14:28 +00:00
wh1te909
4fb6d9aa5d more docs 2021-02-24 05:32:16 +00:00
sadnub
f092ea8d67 black 2021-02-23 23:58:28 -05:00
sadnub
c32cbbdda6 check run tests and agent alert actions tests 2021-02-23 23:53:55 -05:00
sadnub
2497675259 UI changes for AddAutomated Task and ScriptCheck models 2021-02-23 23:53:55 -05:00
sadnub
8d084ab90a docker dev changes 2021-02-23 23:53:55 -05:00
wh1te909
2398773ef0 moar docs 2021-02-24 03:33:39 +00:00
wh1te909
a05998a30e docs 2021-02-24 00:12:55 +00:00
wh1te909
f863c29194 more docs 2021-02-23 22:19:58 +00:00
wh1te909
d16a98c788 Release 0.4.17 2021-02-23 19:26:54 +00:00
wh1te909
9421b02e96 bump versions 2021-02-23 19:26:17 +00:00
wh1te909
10256864e4 improve typing support 2021-02-23 09:50:57 +00:00
wh1te909
85d010615d black 2021-02-23 08:27:22 +00:00
wh1te909
cd1cb186be deploy docs with gh actions 2021-02-23 08:24:19 +00:00
wh1te909
4458354d70 more docs 2021-02-23 08:14:25 +00:00
wh1te909
0f27da8808 add management command to show outdated agents 2021-02-22 20:31:57 +00:00
wh1te909
dd76bfa3c2 fix python build from source 2021-02-22 10:06:47 +00:00
wh1te909
5780a66f7d fix python build from source 2021-02-22 10:05:46 +00:00
wh1te909
d4342c034c add test for run_script 2021-02-22 09:46:48 +00:00
wh1te909
1ec43f2530 refactor to remove duplicate code 2021-02-22 08:46:59 +00:00
wh1te909
3c300d8fdf remove print 2021-02-22 08:45:57 +00:00
wh1te909
23119b55d1 isort 2021-02-22 08:43:21 +00:00
wh1te909
c8fb0e8f8a remove unneeded imports that are now builtin in python 3.9 2021-02-22 08:05:30 +00:00
sadnub
0ec32a77ef make check results chart more responsive with large amounts of data 2021-02-21 19:00:43 -05:00
sadnub
52921bfce8 black 2021-02-21 18:56:14 -05:00
sadnub
960b929097 move annotation labels to the left for check history chart 2021-02-21 18:51:45 -05:00
sadnub
d4ce23eced adding tests to agent alert actions and a bunch of fixes 2021-02-21 18:45:34 -05:00
wh1te909
6925510f44 no cgo 2021-02-21 10:18:05 +00:00
wh1te909
9827ad4c22 add isort to dev reqs 2021-02-21 10:17:47 +00:00
wh1te909
ef8aaee028 Release 0.4.16 2021-02-21 09:58:41 +00:00
wh1te909
3d7d39f248 bump version 2021-02-21 09:58:28 +00:00
wh1te909
3eac620560 add go mod to fix docker agent exe 2021-02-21 09:56:16 +00:00
wh1te909
ab17006956 Release 0.4.15 2021-02-21 08:37:01 +00:00
wh1te909
bfc6889ee9 bump version 2021-02-21 08:36:44 +00:00
wh1te909
0ec0b4a044 python 3.9 2021-02-21 07:57:36 +00:00
wh1te909
f1a523f327 update reqs 2021-02-21 07:37:36 +00:00
sadnub
4181449aea fix tests 2021-02-20 23:18:54 -05:00
sadnub
e192f8db52 dont create alerts if not configured to do so. Added some more tests 2021-02-20 23:01:19 -05:00
wh1te909
8097c681ac Release 0.4.14 2021-02-20 22:35:35 +00:00
wh1te909
f45938bdd5 bump version 2021-02-20 22:35:14 +00:00
wh1te909
6ea4e97eca fix script args 2021-02-20 22:33:10 +00:00
wh1te909
f274c8e837 add prune alerts to server maintenance tool 2021-02-20 11:01:04 +00:00
wh1te909
335e571485 add optional --force flag to update.sh 2021-02-20 10:33:21 +00:00
wh1te909
a11616aace Release 0.4.13 2021-02-20 10:15:51 +00:00
wh1te909
883acadbc4 bump versions 2021-02-20 10:00:12 +00:00
wh1te909
f51e6a3fcf isort imports 2021-02-20 09:54:01 +00:00
wh1te909
371e081c0d remove un-used imports 2021-02-20 09:47:19 +00:00
wh1te909
6f41b3bf1c change wording 2021-02-20 09:36:36 +00:00
wh1te909
c1d74a6c9e improve alerts manager table UI 2021-02-20 08:56:19 +00:00
wh1te909
24eaa6796e remove old field 2021-02-20 08:40:06 +00:00
wh1te909
1521e3b620 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-02-20 03:44:59 +00:00
wh1te909
b6ff38dd62 fix date sorting and timezone fixes #283 2021-02-20 03:44:42 +00:00
sadnub
44ea9ac03c black 2021-02-19 22:43:48 -05:00
wh1te909
4c2701505b Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-02-20 03:42:35 +00:00
sadnub
9022fe18da add some alerts tests and some fixes 2021-02-19 22:40:00 -05:00
wh1te909
63be349f8b update quasar 2021-02-20 03:37:30 +00:00
Tragic Bronson
c40256a290 Merge pull request #286 from bradhawkins85/patch-5
Update installer.ps1
2021-02-19 00:52:09 -08:00
bradhawkins85
33ecb8ec52 Update installer.ps1
Add windows defender exclusions before downloading or installing the agent.
2021-02-19 18:04:24 +10:00
wh1te909
82d62a0015 improve mesh update detection 2021-02-18 08:53:02 +00:00
wh1te909
6278240526 Release 0.4.12 2021-02-18 07:36:31 +00:00
wh1te909
8c2dc5f57d typo 2021-02-18 07:34:28 +00:00
264 changed files with 33132 additions and 57668 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9.2-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_GO_DIR /usr/local/rmmgo

View File

@@ -100,6 +100,7 @@ MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
REDIS_HOST = '${REDIS_HOST}'
ADMIN_ENABLED = True
EOF
)"
@@ -126,7 +127,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# setup Python virtual env and install dependencies
! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
django_setup

View File

@@ -1,40 +1,24 @@
# 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
amqp==5.0.5
asgiref==3.3.1
asyncio-nats-client==0.11.4
billiard==3.6.3.0
celery==5.0.5
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
cryptography==3.4.4
decorator==4.4.2
Django==3.1.6
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.2
future==0.18.2
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.2
packaging==20.8
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.10.1
pyotp==2.6.0
pyparsing==2.4.7
pytz==2021.1
qrcode==6.1
redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.52.0
urllib3==1.26.3
validators==0.18.2
vine==5.0.0
websockets==8.1
zipp==3.4.0
asyncio-nats-client
celery
Django
django-cors-headers
django-rest-knox
djangorestframework
loguru
msgpack
psycopg2-binary
pycparser
pycryptodome
pyotp
pyparsing
pytz
qrcode
redis
twilio
packaging
validators
websockets
black
Werkzeug
django-extensions
@@ -44,3 +28,5 @@ model_bakery
mkdocs
mkdocs-material
pymdown-extensions
Pygments
mypy

2
.github/FUNDING.yml vendored
View File

@@ -3,7 +3,7 @@
github: wh1te909
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
ko_fi: tacticalrmm
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username

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

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

2
.gitignore vendored
View File

@@ -45,3 +45,5 @@ htmlcov/
docker-compose.dev.yml
docs/.vuepress/dist
nats-rmm.conf
.mypy_cache
docs/site/

View File

@@ -3,7 +3,14 @@
"python.languageServer": "Pylance",
"python.analysis.extraPaths": [
"api/tacticalrmm",
"api/env",
],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
},
"python.analysis.memory.keepLibraryAst": true,
"python.linting.mypyEnabled": true,
"python.analysis.typeCheckingMode": "basic",
"python.formatting.provider": "black",
"editor.formatOnSave": true,

View File

@@ -15,6 +15,8 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
### [Discord Chat](https://discord.gg/upGTkWp)
### [Documentation](https://wh1te909.github.io/tacticalrmm/)
## Features
- Teamviewer-like remote desktop control
@@ -33,98 +35,6 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
## Installation
## Installation / Backup / Restore / Usage
### Requirements
- VPS with 2GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
- A domain you own with at least 3 subdomains
- Google Authenticator app (2 factor is NOT optional)
### Docker
Refer to the [docker setup](docker/readme.md)
### Installation example (Ubuntu server 20.04 LTS)
Fresh VPS with latest updates\
login as root and create a user and add to sudoers group (we will be creating a user called tactical)
```
apt update && apt -y upgrade
adduser tactical
usermod -a -G sudo tactical
```
switch to the tactical user and setup the firewall
```
su - tactical
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow proto tcp from any to any port 4222
sudo ufw enable && sudo ufw reload
```
Our domain for this example is tacticalrmm.com
In the DNS manager of wherever our domain is hosted, we will create three A records, all pointing to the public IP address of our VPS
Create A record ```api.tacticalrmm.com``` for the django rest backend\
Create A record ```rmm.tacticalrmm.com``` for the vue frontend\
Create A record ```mesh.tacticalrmm.com``` for meshcentral
Download the install script and run it
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh
chmod +x install.sh
./install.sh
```
Links will be provided at the end of the install script.\
Download the executable from the first link, then open ```rmm.tacticalrmm.com``` and login.\
Upload the executable when prompted during the initial setup page.
### Install an agent
From the app's dashboard, choose Agents > Install Agent to generate an installer.
## Updating
Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh)
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
chmod +x update.sh
./update.sh
```
## Backup
Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh)
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
```
Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section)
Run it
```
chmod +x backup.sh
./backup.sh
```
## Restore
Change your 3 A records to point to new server's public IP
Create same linux user account as old server and add to sudoers group and setup firewall (see install instructions above)
Copy backup file to new server
Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps.
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
```
Run the restore script, passing it the backup tar file as the first argument
```
chmod +x restore.sh
./restore.sh rmm-backup-xxxxxxx.tar
```
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)

View File

@@ -1,5 +1,4 @@
from django.contrib import admin
from rest_framework.authtoken.admin import TokenAdmin
from .models import User

View File

@@ -1,6 +1,5 @@
from django.utils import timezone as djangotime
from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime
from knox.models import AuthToken

View File

@@ -1,11 +1,13 @@
import pyotp
import subprocess
import pyotp
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Generates barcode for Google Authenticator and creates totp for user"
help = "Generates barcode for Authenticator and creates totp for user"
def add_arguments(self, parser):
parser.add_argument("code", type=str)
@@ -24,12 +26,10 @@ class Command(BaseCommand):
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
subprocess.run(f'qr "{url}"', shell=True)
self.stdout.write(
self.style.SUCCESS(
"Scan the barcode above with your google authenticator app"
)
self.style.SUCCESS("Scan the barcode above with your authenticator app")
)
self.stdout.write(
self.style.SUCCESS(
f"If that doesn't work you may manually enter the key: {code}"
f"If that doesn't work you may manually enter the setup key: {code}"
)
)

View File

@@ -0,0 +1,57 @@
import os
import subprocess
import pyotp
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Reset 2fa"
def add_arguments(self, parser):
parser.add_argument("username", type=str)
def handle(self, *args, **kwargs):
username = kwargs["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
domain = "Tactical RMM"
nginx = "/etc/nginx/sites-available/frontend.conf"
found = None
if os.path.exists(nginx):
try:
with open(nginx, "r") as f:
for line in f:
if "server_name" in line:
found = line
break
if found:
rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32()
user.totp_key = code
user.save(update_fields=["totp_key"])
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
subprocess.run(f'qr "{url}"', shell=True)
self.stdout.write(
self.style.WARNING("Scan the barcode above with your authenticator app")
)
self.stdout.write(
self.style.WARNING(
f"If that doesn't work you may manually enter the setup key: {code}"
)
)
self.stdout.write(
self.style.SUCCESS(f"2fa was successfully reset for user {username}")
)

View File

@@ -0,0 +1,22 @@
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Reset password for user"
def add_arguments(self, parser):
parser.add_argument("username", type=str)
def handle(self, *args, **kwargs):
username = kwargs["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
passwd = input("Enter new password: ")
user.set_password(passwd)
user.save()
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))

View File

@@ -2,8 +2,8 @@
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.2 on 2020-11-10 20:24
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-02-28 06:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0011_user_default_agent_tbl_tab'),
]
operations = [
migrations.AddField(
model_name='user',
name='agents_per_page',
field=models.PositiveIntegerField(default=50),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-09 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0012_user_agents_per_page'),
]
operations = [
migrations.AddField(
model_name='user',
name='client_tree_sort',
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
),
]

View File

@@ -1,5 +1,5 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.db import models
from logs.models import BaseAuditModel
@@ -15,6 +15,11 @@ AGENT_TBL_TAB_CHOICES = [
("mixed", "Mixed"),
]
CLIENT_TREE_SORT_CHOICES = [
("alphafail", "Move failing clients to the top"),
("alpha", "Sort alphabetically"),
]
class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True)
@@ -27,6 +32,10 @@ class User(AbstractUser, BaseAuditModel):
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="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"
)
agent = models.OneToOneField(
"agents.Agent",

View File

@@ -1,13 +1,21 @@
import pyotp
from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
)
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import User
class UserUISerializer(ModelSerializer):
class Meta:
model = User
fields = [
"dark_mode",
"show_community_scripts",
"agent_dblclick_action",
"default_agent_tbl_tab",
"client_tree_sort",
]
class UserSerializer(ModelSerializer):
class Meta:
model = User

View File

@@ -1,8 +1,9 @@
from unittest.mock import patch
from django.test import override_settings
from tacticalrmm.test import TacticalTestCase
from accounts.models import User
from tacticalrmm.test import TacticalTestCase
class TestAccounts(TacticalTestCase):
@@ -270,18 +271,13 @@ class TestUserAction(TacticalTestCase):
def test_user_ui(self):
url = "/accounts/users/ui/"
data = {"dark_mode": False}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"show_community_scripts": True}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {
"userui": True,
"dark_mode": True,
"show_community_scripts": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
"client_tree_sort": "alpha",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -1,23 +1,28 @@
import pyotp
from django.contrib.auth import login
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.contrib.auth import login
from django.db import IntegrityError
from rest_framework.views import APIView
from rest_framework.authtoken.serializers import AuthTokenSerializer
from django.shortcuts import get_object_or_404
from knox.views import LoginView as KnoxLoginView
from rest_framework import status
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from .models import User
from agents.models import Agent
from logs.models import AuditLog
from tacticalrmm.utils import notify_error
from .serializers import UserSerializer, TOTPSetupSerializer
from .models import User
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
def _is_root_user(request, user) -> bool:
return (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
class CheckCreds(KnoxLoginView):
@@ -81,7 +86,7 @@ class GetAddUsers(APIView):
def post(self, request):
# add new user
try:
user = User.objects.create_user(
user = User.objects.create_user( # type: ignore
request.data["username"],
request.data["email"],
request.data["password"],
@@ -108,11 +113,7 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True)
@@ -123,11 +124,7 @@ class GetUpdateDeleteUser(APIView):
def delete(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
@@ -140,11 +137,7 @@ class UserActions(APIView):
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"])
@@ -155,11 +148,7 @@ class UserActions(APIView):
# reset two factor token
def put(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.totp_key = ""
@@ -187,19 +176,9 @@ class TOTPSetup(APIView):
class UserUI(APIView):
def patch(self, request):
user = request.user
if "dark_mode" in request.data.keys():
user.dark_mode = request.data["dark_mode"]
user.save(update_fields=["dark_mode"])
if "show_community_scripts" in request.data.keys():
user.show_community_scripts = request.data["show_community_scripts"]
user.save(update_fields=["show_community_scripts"])
if "userui" in request.data.keys():
user.agent_dblclick_action = request.data["agent_dblclick_action"]
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
serializer = UserUISerializer(
instance=request.user, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Agent, RecoveryAction, Note
from .models import Agent, Note, RecoveryAction
admin.site.register(Agent)
admin.site.register(RecoveryAction)

View File

@@ -1,12 +1,12 @@
import json
import os
import random
import string
import os
import json
from model_bakery.recipe import Recipe, foreign_key
from itertools import cycle
from django.utils import timezone as djangotime
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery.recipe import Recipe, foreign_key, seq
def generate_agent_id(hostname):
@@ -30,8 +30,7 @@ agent = Recipe(
hostname="DESKTOP-TEST123",
version="1.3.0",
monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
)
server_agent = agent.extend(
@@ -44,8 +43,12 @@ workstation_agent = agent.extend(
online_agent = agent.extend(last_seen=djangotime.now())
offline_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
)
overdue_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=6)
last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
)
agent_with_services = agent.extend(

View File

@@ -0,0 +1,93 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from clients.models import Client, Site
class Command(BaseCommand):
help = "Bulk update agent offline/overdue time"
def add_arguments(self, parser):
parser.add_argument("time", type=int, help="Time in minutes")
parser.add_argument(
"--client",
type=str,
help="Client Name",
)
parser.add_argument(
"--site",
type=str,
help="Site Name",
)
parser.add_argument(
"--offline",
action="store_true",
help="Offline",
)
parser.add_argument(
"--overdue",
action="store_true",
help="Overdue",
)
parser.add_argument(
"--all",
action="store_true",
help="All agents",
)
def handle(self, *args, **kwargs):
time = kwargs["time"]
client_name = kwargs["client"]
site_name = kwargs["site"]
all_agents = kwargs["all"]
offline = kwargs["offline"]
overdue = kwargs["overdue"]
agents = None
if offline and time < 2:
self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
return
if overdue and time < 3:
self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
return
if client_name:
try:
client = Client.objects.get(name=client_name)
except Client.DoesNotExist:
self.stdout.write(
self.style.ERROR(f"Client {client_name} doesn't exist")
)
return
agents = Agent.objects.filter(site__client=client)
elif site_name:
try:
site = Site.objects.get(name=site_name)
except Site.DoesNotExist:
self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
return
agents = Agent.objects.filter(site=site)
elif all_agents:
agents = Agent.objects.all()
if agents:
if offline:
agents.update(offline_time=time)
self.stdout.write(
self.style.SUCCESS(
f"Changed offline time on {len(agents)} agents to {time} minutes"
)
)
if overdue:
agents.update(overdue_time=time)
self.stdout.write(
self.style.SUCCESS(
f"Changed overdue time on {len(agents)} agents to {time} minutes"
)
)

View File

@@ -0,0 +1,18 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from agents.models import Agent
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"]
for agent in agents:
self.stdout.write(
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
)

View File

@@ -1,8 +1,8 @@
# Generated by Django 3.0.6 on 2020-05-31 01:23
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.0.7 on 2020-06-09 16:07
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.0.8 on 2020-08-09 05:31
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,8 +1,8 @@
# Generated by Django 3.1.1 on 2020-09-22 20:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.2 on 2020-11-01 22:53
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-03-04 03:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0006_auto_20210217_1736'),
('agents', '0030_agent_offline_time'),
]
operations = [
migrations.AddField(
model_name='agent',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
),
]

View File

@@ -1,28 +1,26 @@
import time
import base64
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA3_384
from Crypto.Util.Padding import pad
import validators
import msgpack
import re
from collections import Counter
from typing import List, Union, Any
from loguru import logger
import asyncio
from packaging import version as pyver
import base64
import re
import time
from collections import Counter
from distutils.version import LooseVersion
from typing import Any, Union
import msgpack
import validators
from Crypto.Cipher import AES
from Crypto.Hash import SHA3_384
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
from django.conf import settings
from django.db import models
from django.utils import timezone as djangotime
from loguru import logger
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from django.db import models
from django.conf import settings
from django.utils import timezone as djangotime
from alerts.models import AlertTemplate
from core.models import CoreSettings, TZ_CHOICES
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
logger.configure(**settings.LOG_CONFIG)
@@ -65,6 +63,13 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
site = models.ForeignKey(
"clients.Site",
related_name="agents",
@@ -86,7 +91,7 @@ class Agent(BaseAuditModel):
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# check if new agent has been create
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
if (
@@ -165,14 +170,14 @@ class Agent(BaseAuditModel):
@property
def has_patches_pending(self):
return self.winupdates.filter(action="approve").filter(installed=False).exists()
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
@property
def checks(self):
total, passing, failing = 0, 0, 0
if self.agentchecks.exists():
for i in self.agentchecks.all():
if self.agentchecks.exists(): # type: ignore
for i in self.agentchecks.all(): # type: ignore
total += 1
if i.status == "passing":
passing += 1
@@ -242,6 +247,7 @@ class Agent(BaseAuditModel):
pass
try:
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
except:
pass
@@ -271,10 +277,24 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
def check_run_interval(self) -> int:
interval = self.check_interval
# determine if any agent checks have a custom interval and set the lowest interval
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
if check.run_interval and check.run_interval < interval:
# don't allow check runs less than 15s
if check.run_interval < 15:
interval = 15
else:
interval = check.run_interval
return interval
def run_script(
self,
scriptpk: int,
args: List[str] = [],
args: list[str] = [],
timeout: int = 120,
full: bool = False,
wait: bool = False,
@@ -296,10 +316,10 @@ class Agent(BaseAuditModel):
running_agent = self
if run_on_any:
nats_ping = {"func": "ping", "timeout": 1}
nats_ping = {"func": "ping"}
# try on self first
r = asyncio.run(self.nats_cmd(nats_ping))
r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
if r == "pong":
running_agent = self
@@ -313,7 +333,7 @@ class Agent(BaseAuditModel):
]
for agent in online:
r = asyncio.run(agent.nats_cmd(nats_ping))
r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
if r == "pong":
running_agent = agent
break
@@ -334,27 +354,27 @@ class Agent(BaseAuditModel):
updates = list()
if patch_policy.critical == "approve":
updates += self.winupdates.filter(
updates += self.winupdates.filter( # type: ignore
severity="Critical", installed=False
).exclude(action="approve")
if patch_policy.important == "approve":
updates += self.winupdates.filter(
updates += self.winupdates.filter( # type: ignore
severity="Important", installed=False
).exclude(action="approve")
if patch_policy.moderate == "approve":
updates += self.winupdates.filter(
updates += self.winupdates.filter( # type: ignore
severity="Moderate", installed=False
).exclude(action="approve")
if patch_policy.low == "approve":
updates += self.winupdates.filter(severity="Low", installed=False).exclude(
updates += self.winupdates.filter(severity="Low", installed=False).exclude( # type: ignore
action="approve"
)
if patch_policy.other == "approve":
updates += self.winupdates.filter(severity="", installed=False).exclude(
updates += self.winupdates.filter(severity="", installed=False).exclude( # type: ignore
action="approve"
)
@@ -369,7 +389,7 @@ class Agent(BaseAuditModel):
site = self.site
core_settings = CoreSettings.objects.first()
patch_policy = None
agent_policy = self.winupdatepolicy.get()
agent_policy = self.winupdatepolicy.get() # type: ignore
if self.monitoring_type == "server":
# check agent policy first which should override client or site policy
@@ -454,16 +474,16 @@ class Agent(BaseAuditModel):
return patch_policy
def get_approved_update_guids(self) -> List[str]:
def get_approved_update_guids(self) -> list[str]:
return list(
self.winupdates.filter(action="approve", installed=False).values_list(
self.winupdates.filter(action="approve", installed=False).values_list( # type: ignore
"guid", flat=True
)
)
# returns alert template assigned in the following order: policy, site, client, global
# will return None if nothing is found
def get_alert_template(self) -> Union[AlertTemplate, None]:
# sets alert template assigned in the following order: policy, site, client, global
# sets None if nothing is found
def set_alert_template(self):
site = self.site
client = self.client
@@ -479,14 +499,14 @@ class Agent(BaseAuditModel):
templates.append(self.policy.alert_template)
# check if policy with alert template is assigned to the site
elif (
if (
self.monitoring_type == "server"
and site.server_policy
and site.server_policy.alert_template
and site.server_policy.alert_template.is_active
):
templates.append(site.server_policy.alert_template)
elif (
if (
self.monitoring_type == "workstation"
and site.workstation_policy
and site.workstation_policy.alert_template
@@ -495,18 +515,18 @@ class Agent(BaseAuditModel):
templates.append(site.workstation_policy.alert_template)
# check if alert template is assigned to site
elif site.alert_template and site.alert_template.is_active:
if site.alert_template and site.alert_template.is_active:
templates.append(site.alert_template)
# check if policy with alert template is assigned to the client
elif (
if (
self.monitoring_type == "server"
and client.server_policy
and client.server_policy.alert_template
and client.server_policy.alert_template.is_active
):
templates.append(client.server_policy.alert_template)
elif (
if (
self.monitoring_type == "workstation"
and client.workstation_policy
and client.workstation_policy.alert_template
@@ -515,22 +535,22 @@ class Agent(BaseAuditModel):
templates.append(client.workstation_policy.alert_template)
# check if alert template is on client and return
elif client.alert_template and client.alert_template.is_active:
if client.alert_template and client.alert_template.is_active:
templates.append(client.alert_template)
# check if alert template is applied globally and return
elif core.alert_template and core.alert_template.is_active:
if core.alert_template and core.alert_template.is_active:
templates.append(core.alert_template)
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
elif (
if (
self.monitoring_type == "server"
and core.server_policy
and core.server_policy.alert_template
and core.server_policy.alert_template.is_active
):
templates.append(core.server_policy.alert_template)
elif (
if (
self.monitoring_type == "workstation"
and core.workstation_policy
and core.workstation_policy.alert_template
@@ -553,23 +573,33 @@ class Agent(BaseAuditModel):
continue
# check if template is excluding desktops
if self.monitoring_type == "workstation" and template.exclude_desktops:
elif (
self.monitoring_type == "workstation" and template.exclude_workstations
):
continue
# check if template is excluding servers
elif self.monitoring_type == "server" and template.exclude_servers:
continue
else:
# save alert_template to agent cache field
self.alert_template = template
self.save()
return template
# no alert templates found or agent has been excluded
self.alert_template = None
self.save()
return None
def generate_checks_from_policies(self):
from automation.models import Policy
# Clear agent checks that have overriden_by_policy set
self.agentchecks.update(overriden_by_policy=False)
self.agentchecks.update(overriden_by_policy=False) # type: ignore
# Generate checks based on policies
Policy.generate_policy_checks(self)
@@ -604,7 +634,7 @@ class Agent(BaseAuditModel):
except Exception:
return "err"
async def nats_cmd(self, data, timeout=30, wait=True):
async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True):
nc = NATS()
options = {
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
@@ -626,7 +656,7 @@ class Agent(BaseAuditModel):
except ErrTimeout:
ret = "timeout"
else:
ret = msgpack.loads(msg.data)
ret = msgpack.loads(msg.data) # type: ignore
await nc.close()
return ret
@@ -648,12 +678,12 @@ class Agent(BaseAuditModel):
def delete_superseded_updates(self):
try:
pks = [] # list of pks to delete
kbs = list(self.winupdates.values_list("kb", flat=True))
kbs = list(self.winupdates.values_list("kb", flat=True)) # type: ignore
d = Counter(kbs)
dupes = [k for k, v in d.items() if v > 1]
for dupe in dupes:
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True) # type: ignore
# extract the version from the title and sort from oldest to newest
# skip if no version info is available therefore nothing to parse
try:
@@ -666,24 +696,24 @@ class Agent(BaseAuditModel):
continue
# append all but the latest version to our list of pks to delete
for ver in sorted_vers[:-1]:
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver) # type: ignore
pks.append(q.first().pk)
pks = list(set(pks))
self.winupdates.filter(pk__in=pks).delete()
self.winupdates.filter(pk__in=pks).delete() # type: ignore
except:
pass
# define how the agent should handle pending actions
def handle_pending_actions(self):
pending_actions = self.pendingactions.filter(status="pending")
pending_actions = self.pendingactions.filter(status="pending") # type: ignore
for action in pending_actions:
if action.action_type == "taskaction":
from autotasks.tasks import (
create_win_task_schedule,
enable_or_disable_win_task,
delete_win_task_schedule,
enable_or_disable_win_task,
)
task_id = action.details["task_id"]
@@ -700,160 +730,29 @@ class Agent(BaseAuditModel):
# for clearing duplicate pending actions on agent
def remove_matching_pending_task_actions(self, task_id):
# remove any other pending actions on agent with same task_id
for action in self.pendingactions.exclude(status="completed"):
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
if action.details["task_id"] == task_id:
action.delete()
def handle_alert(self, checkin: bool = False) -> None:
from alerts.models import Alert
from agents.tasks import (
agent_recovery_email_task,
agent_recovery_sms_task,
agent_outage_email_task,
agent_outage_sms_task,
)
# return if agent is in maintenace mode
if self.maintenance_mode:
return
alert_template = self.get_alert_template()
# called when agent is back online
if checkin:
if Alert.objects.filter(agent=self, resolved=False).exists():
# resolve alert if exists
alert = Alert.objects.get(agent=self, resolved=False)
alert.resolve()
# check if a resolved notification should be emailed
if (
not alert.resolved_email_sent
and alert_template
and alert_template.agent_email_on_resolved
or self.overdue_email_alert
):
agent_recovery_email_task.delay(pk=alert.pk)
# check if a resolved notification should be texted
if (
not alert.resolved_sms_sent
and alert_template
and alert_template.agent_text_on_resolved
or self.overdue_text_alert
):
agent_recovery_sms_task.delay(pk=alert.pk)
# check if any scripts should be run
if (
not alert.resolved_action_run
and alert_template
and alert_template.resolved_action
):
r = self.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
)
# called when agent is offline
else:
# check if alert hasn't been created yet so create it
if not Alert.objects.filter(agent=self, resolved=False).exists():
alert = Alert.create_availability_alert(self)
# add a null check history to allow gaps in graph
for check in self.agentchecks.all():
check.add_check_history(None)
else:
alert = Alert.objects.get(agent=self, resolved=False)
# create dashboard alert if enabled
if (
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert
or self.overdue_email_alert
or self.overdue_text_alert
or (
alert_template
and alert_template.agent_always_alert
or self.overdue_dashboard_alert
):
alert.hidden = False
alert.save()
# send email alert if enabled
if (
not alert.email_sent
and alert_template
and alert_template.agent_always_email
or self.overdue_email_alert
):
agent_outage_email_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
and (
alert_template.agent_always_alert
or alert_template.agent_always_email
or alert_template.agent_always_text
)
# send text message if enabled
if (
not alert.sms_sent
and alert_template
and alert_template.agent_always_text
or self.overdue_text_alert
):
agent_outage_sms_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if not alert.action_run and alert_template and alert_template.action:
r = self.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
)
)
)
def send_outage_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
@@ -862,14 +761,13 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"within the expected time."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
(
@@ -878,27 +776,25 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"after an interruption in data transmission."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_outage_sms(self):
from core.models import CoreSettings
alert_template = self.get_alert_template()
CORE = CoreSettings.objects.first()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
alert_template=self.alert_template,
)

View File

@@ -1,13 +1,11 @@
import pytz
from rest_framework import serializers
from rest_framework.fields import ReadOnlyField
from clients.serializers import ClientSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, Note
from winupdate.serializers import WinUpdatePolicySerializer
from clients.serializers import ClientSerializer
class AgentSerializer(serializers.ModelSerializer):
# for vue
@@ -59,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
alert_template = obj.get_alert_template()
if not alert_template:
if not obj.alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.agent_always_email,
"always_text": alert_template.agent_always_text,
"always_alert": alert_template.agent_always_alert,
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
def get_pending_actions(self, obj):
@@ -80,7 +77,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
else:
agent_tz = self.context["default_tz"]
return obj.last_seen.astimezone(agent_tz).timestamp()
return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M")
def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == "online":

View File

@@ -1,19 +1,19 @@
import asyncio
from loguru import logger
from time import sleep
import random
from packaging import version as pyver
from typing import List, Union
import datetime as dt
import random
from time import sleep
from typing import Union
from django.utils import timezone as djangotime
from django.conf import settings
from scripts.models import Script
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from tacticalrmm.celery import app
from agents.models import Agent
from core.models import CoreSettings
from logs.models import PendingAction
from scripts.models import Script
from tacticalrmm.celery import app
logger.configure(**settings.LOG_CONFIG)
@@ -77,7 +77,7 @@ def agent_update(pk: int) -> str:
@app.task
def send_agent_update_task(pks: List[int]) -> None:
def send_agent_update_task(pks: list[int]) -> None:
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
for chunk in chunks:
for pk in chunk:
@@ -93,7 +93,7 @@ def auto_self_agent_update_task() -> None:
return
q = Agent.objects.only("pk", "version")
pks: List[int] = [
pks: list[int] = [
i.pk
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
@@ -183,6 +183,8 @@ def agent_recovery_sms_task(pk: int) -> str:
@app.task
def agent_outages_task() -> None:
from alerts.models import Alert
agents = Agent.objects.only(
"pk",
"last_seen",
@@ -195,30 +197,22 @@ def agent_outages_task() -> None:
for agent in agents:
if agent.status == "overdue":
agent.handle_alert()
@app.task
def handle_agent_recovery_task(pk: int) -> None:
sleep(10)
from agents.models import RecoveryAction
action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}
asyncio.run(action.agent.nats_cmd(data, wait=False))
Alert.handle_alert_failure(agent)
@app.task
def run_script_email_results_task(
agentpk: int, scriptpk: int, nats_timeout: int, emails: List[str]
agentpk: int,
scriptpk: int,
nats_timeout: int,
emails: list[str],
args: list[str] = [],
):
agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk)
r = agent.run_script(scriptpk=script.pk, full=True, timeout=nats_timeout, wait=True)
r = agent.run_script(
scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True
)
if r == "timeout":
logger.error(f"{agent.hostname} timed out running script.")
return

View File

@@ -1,23 +1,77 @@
import json
import os
from itertools import cycle
from unittest.mock import patch
from django.conf import settings
from model_bakery import baker
from itertools import cycle
from typing import List
from packaging import version as pyver
from django.conf import settings
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .tasks import auto_self_agent_update_task
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .serializers import AgentSerializer
from .tasks import auto_self_agent_update_task
class TestAgentsList(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_agents_list(self):
url = "/agents/listagents/"
# 36 total agents
company1 = baker.make("clients.Client")
company2 = baker.make("clients.Client")
site1 = baker.make("clients.Site", client=company1)
site2 = baker.make("clients.Site", client=company1)
site3 = baker.make("clients.Site", client=company2)
baker.make_recipe(
"agents.online_agent", site=site1, monitoring_type="server", _quantity=15
)
baker.make_recipe(
"agents.online_agent",
site=site2,
monitoring_type="workstation",
_quantity=10,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="server",
_quantity=4,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="workstation",
_quantity=7,
)
# test all agents
r = self.client.patch(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 36) # type: ignore
# test client1
data = {"clientPK": company1.pk} # type: ignore
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 25) # type: ignore
# test site3
data = {"sitePK": site3.pk} # type: ignore
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 11) # type: ignore
self.check_not_authenticated("patch", url)
class TestAgentViews(TacticalTestCase):
@@ -80,12 +134,12 @@ class TestAgentViews(TacticalTestCase):
_quantity=15,
)
pks: List[int] = list(
pks: list[int] = list(
Agent.objects.only("pk", "version").values_list("pk", flat=True)
)
data = {"pks": pks}
expected: List[int] = [
expected: list[int] = [
i.pk
for i in Agent.objects.only("pk", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
@@ -259,7 +313,7 @@ class TestAgentViews(TacticalTestCase):
mock_ret.return_value = "nt authority\system"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertIsInstance(r.data, str)
self.assertIsInstance(r.data, str) # type: ignore
mock_ret.return_value = "timeout"
r = self.client.post(url, data, format="json")
@@ -279,15 +333,15 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.return_value = "ok"
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["agent"], self.agent.hostname)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") # type: ignore
self.assertEqual(r.data["agent"], self.agent.hostname) # type: ignore
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "schedreboot",
"trigger": "once",
"name": r.data["task_name"],
"name": r.data["task_name"], # type: ignore
"year": 2025,
"month": "August",
"day": 29,
@@ -308,7 +362,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.patch(url, data_invalid, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Invalid date")
self.assertEqual(r.data, "Invalid date") # type: ignore
self.check_not_authenticated("patch", url)
@@ -319,8 +373,8 @@ class TestAgentViews(TacticalTestCase):
site = baker.make("clients.Site")
data = {
"client": site.client.id,
"site": site.id,
"client": site.client.id, # type: ignore
"site": site.id, # type: ignore
"arch": "64",
"expires": 23,
"installMethod": "exe",
@@ -367,50 +421,69 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_recover(self):
@patch("agents.models.Agent.nats_cmd")
def test_recover(self, nats_cmd):
from agents.models import RecoveryAction
self.agent.version = "0.11.1"
self.agent.save(update_fields=["version"])
RecoveryAction.objects.all().delete()
url = "/agents/recover/"
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
agent = baker.make_recipe("agents.online_agent")
# test mesh realtime
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
)
nats_cmd.reset_mock()
data["mode"] = "mesh"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json())
RecoveryAction.objects.all().delete()
data["mode"] = "command"
data["cmd"] = "ipconfig /flushdns"
# test mesh with agent rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
RecoveryAction.objects.all().delete()
data["cmd"] = None
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 1)
mesh_recovery = RecoveryAction.objects.first()
self.assertEqual(mesh_recovery.mode, "mesh")
nats_cmd.reset_mock()
RecoveryAction.objects.all().delete()
self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"])
data["mode"] = "mesh"
# test tacagent realtime
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json())
self.check_not_authenticated("post", url)
def test_agents_list(self):
url = "/agents/listagents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
)
nats_cmd.reset_mock()
self.check_not_authenticated("get", url)
# test tacagent with rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.reset_mock()
# test shell cmd without command
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
# test shell cmd
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 1)
cmd_recovery = RecoveryAction.objects.first()
self.assertEqual(cmd_recovery.mode, "command")
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
def test_agents_agent_detail(self):
url = f"/agents/{self.agent.pk}/agentdetail/"
@@ -428,7 +501,7 @@ class TestAgentViews(TacticalTestCase):
edit = {
"id": self.agent.pk,
"site": site.id,
"site": site.id, # type: ignore
"monitoring_type": "workstation",
"description": "asjdk234andasd",
"offline_time": 4,
@@ -459,7 +532,7 @@ class TestAgentViews(TacticalTestCase):
agent = Agent.objects.get(pk=self.agent.pk)
data = AgentSerializer(agent).data
self.assertEqual(data["site"], site.id)
self.assertEqual(data["site"], site.id) # type: ignore
policy = WinUpdatePolicy.objects.get(agent=self.agent)
data = WinUpdatePolicySerializer(policy).data
@@ -477,21 +550,21 @@ class TestAgentViews(TacticalTestCase):
# TODO
# decode the cookie
self.assertIn("&viewmode=13", r.data["file"])
self.assertIn("&viewmode=12", r.data["terminal"])
self.assertIn("&viewmode=11", r.data["control"])
self.assertIn("&viewmode=13", r.data["file"]) # type: ignore
self.assertIn("&viewmode=12", r.data["terminal"]) # type: ignore
self.assertIn("&viewmode=11", r.data["control"]) # type: ignore
self.assertIn("&gotonode=", r.data["file"])
self.assertIn("&gotonode=", r.data["terminal"])
self.assertIn("&gotonode=", r.data["control"])
self.assertIn("&gotonode=", r.data["file"]) # type: ignore
self.assertIn("&gotonode=", r.data["terminal"]) # type: ignore
self.assertIn("&gotonode=", r.data["control"]) # type: ignore
self.assertIn("?login=", r.data["file"])
self.assertIn("?login=", r.data["terminal"])
self.assertIn("?login=", r.data["control"])
self.assertIn("?login=", r.data["file"]) # type: ignore
self.assertIn("?login=", r.data["terminal"]) # type: ignore
self.assertIn("?login=", r.data["control"]) # type: ignore
self.assertEqual(self.agent.hostname, r.data["hostname"])
self.assertEqual(self.agent.client.name, r.data["client"])
self.assertEqual(self.agent.site.name, r.data["site"])
self.assertEqual(self.agent.hostname, r.data["hostname"]) # type: ignore
self.assertEqual(self.agent.client.name, r.data["client"]) # type: ignore
self.assertEqual(self.agent.site.name, r.data["site"]) # type: ignore
self.assertEqual(r.status_code, 200)
@@ -501,32 +574,6 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_by_client(self):
url = f"/agents/byclient/{self.agent.client.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/byclient/500/"
r = self.client.get(url)
self.assertFalse(r.data) # returns empty list
self.check_not_authenticated("get", url)
def test_by_site(self):
url = f"/agents/bysite/{self.agent.site.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/bysite/500/"
r = self.client.get(url)
self.assertEqual(r.data, [])
self.check_not_authenticated("get", url)
def test_overdue_action(self):
url = "/agents/overdueaction/"
@@ -535,14 +582,14 @@ class TestAgentViews(TacticalTestCase):
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
self.assertEqual(self.agent.hostname, r.data) # type: ignore
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
self.assertEqual(self.agent.hostname, r.data) # type: ignore
self.check_not_authenticated("post", url)
@@ -686,7 +733,7 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.return_value = "ok"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn(self.agent.hostname, r.data)
self.assertIn(self.agent.hostname, r.data) # type: ignore
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
)
@@ -701,6 +748,77 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.tasks.run_script_email_results_task.delay")
@patch("agents.models.Agent.run_script")
def test_run_script(self, run_script, email_task):
run_script.return_value = "ok"
url = "/agents/runscript/"
script = baker.make_recipe("scripts.script")
# test wait
data = {
"pk": self.agent.pk,
"scriptPK": script.pk,
"output": "wait",
"args": [],
"timeout": 15,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
run_script.assert_called_with(
scriptpk=script.pk, args=[], timeout=18, wait=True
)
run_script.reset_mock()
# test email default
data = {
"pk": self.agent.pk,
"scriptPK": script.pk,
"output": "email",
"args": ["abc", "123"],
"timeout": 15,
"emailmode": "default",
"emails": ["admin@example.com", "bob@example.com"],
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
email_task.assert_called_with(
agentpk=self.agent.pk,
scriptpk=script.pk,
nats_timeout=18,
emails=[],
args=["abc", "123"],
)
email_task.reset_mock()
# test email overrides
data["emailmode"] = "custom"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
email_task.assert_called_with(
agentpk=self.agent.pk,
scriptpk=script.pk,
nats_timeout=18,
emails=["admin@example.com", "bob@example.com"],
args=["abc", "123"],
)
# test fire and forget
data = {
"pk": self.agent.pk,
"scriptPK": script.pk,
"output": "forget",
"args": ["hello", "world"],
"timeout": 22,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
run_script.assert_called_with(
scriptpk=script.pk, args=["hello", "world"], timeout=25
)
class TestAgentViewsNew(TacticalTestCase):
def setUp(self):
@@ -732,7 +850,7 @@ class TestAgentViewsNew(TacticalTestCase):
r = self.client.post(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, data)
self.assertEqual(r.data, data) # type: ignore
self.check_not_authenticated("post", url)
@@ -744,14 +862,14 @@ class TestAgentViewsNew(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site)
# Test client toggle maintenance mode
data = {"type": "Client", "id": site.client.id, "action": True}
data = {"type": "Client", "id": site.client.id, "action": True} # type: ignore
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
# Test site toggle maintenance mode
data = {"type": "Site", "id": site.id, "action": False}
data = {"type": "Site", "id": site.id, "action": False} # type: ignore
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -1,12 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path("listagents/", views.AgentsTableList.as_view()),
path("listagentsnodetail/", views.list_agents_no_detail),
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
path("byclient/<int:clientpk>/", views.by_client),
path("bysite/<int:sitepk>/", views.by_site),
path("overdueaction/", views.overdue_action),
path("sendrawcmd/", views.send_raw_cmd),
path("<pk>/agentdetail/", views.agent_detail),

View File

@@ -1,47 +1,43 @@
import asyncio
from loguru import logger
import datetime as dt
import os
import subprocess
import pytz
import random
import string
import datetime as dt
from packaging import version as pyver
from typing import List
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from loguru import logger
from packaging import version as pyver
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, generics
from rest_framework.views import APIView
from .models import Agent, RecoveryAction, Note
from core.models import CoreSettings
from scripts.models import Script
from logs.models import AuditLog, PendingAction
from .serializers import (
AgentSerializer,
AgentHostnameSerializer,
AgentTableSerializer,
AgentEditSerializer,
NoteSerializer,
NotesSerializer,
AgentOverdueActionSerializer,
from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import (
generate_installer_exe,
get_default_timezone,
notify_error,
reload_nats,
)
from winupdate.serializers import WinUpdatePolicySerializer
from .tasks import (
send_agent_update_task,
run_script_email_results_task,
)
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import notify_error, reload_nats
from .models import Agent, Note, RecoveryAction
from .serializers import (
AgentEditSerializer,
AgentHostnameSerializer,
AgentOverdueActionSerializer,
AgentSerializer,
AgentTableSerializer,
NoteSerializer,
NotesSerializer,
)
from .tasks import run_script_email_results_task, send_agent_update_task
logger.configure(**settings.LOG_CONFIG)
@@ -60,7 +56,7 @@ def get_agent_versions(request):
@api_view(["POST"])
def update_agents(request):
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
pks: List[int] = [
pks: list[int] = [
i.pk
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
@@ -102,7 +98,7 @@ def edit_agent(request):
a_serializer.save()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get()
policy = agent.winupdatepolicy.get() # type: ignore
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
@@ -228,15 +224,32 @@ def send_raw_cmd(request):
return Response(r)
class AgentsTableList(generics.ListAPIView):
queryset = (
Agent.objects.select_related("site")
.prefetch_related("agentchecks")
.only(
class AgentsTableList(APIView):
def patch(self, request):
if "sitePK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site_id=request.data["sitePK"])
)
elif "clientPK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site__client_id=request.data["clientPK"])
)
else:
queryset = Agent.objects.select_related(
"site", "policy", "alert_template"
).prefetch_related("agentchecks")
queryset = queryset.only(
"pk",
"hostname",
"agent_id",
"site",
"policy",
"alert_template",
"monitoring_type",
"description",
"needs_reboot",
@@ -251,14 +264,7 @@ class AgentsTableList(generics.ListAPIView):
"time_zone",
"maintenance_mode",
)
)
serializer_class = AgentTableSerializer
def list(self, request):
queryset = self.get_queryset()
ctx = {
"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)
}
ctx = {"default_tz": get_default_timezone()}
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
return Response(serializer.data)
@@ -275,66 +281,6 @@ def agent_edit_details(request, pk):
return Response(AgentEditSerializer(agent).data)
@api_view()
def by_client(request, clientpk):
agents = (
Agent.objects.select_related("site")
.filter(site__client_id=clientpk)
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
)
)
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@api_view()
def by_site(request, sitepk):
agents = (
Agent.objects.filter(site_id=sitepk)
.select_related("site")
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
)
)
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@api_view(["POST"])
def overdue_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
@@ -436,124 +382,20 @@ def install_agent(request):
)
if request.data["installMethod"] == "exe":
go_bin = "/usr/local/rmmgo/go/bin/go"
if not os.path.exists(go_bin):
return Response("nogolang", status=status.HTTP_409_CONFLICT)
api = request.data["api"]
atype = request.data["agenttype"]
rdp = request.data["rdp"]
ping = request.data["ping"]
power = request.data["power"]
file_name = "rmm-installer.exe"
exe = os.path.join(settings.EXE_DIR, file_name)
if os.path.exists(exe):
try:
os.remove(exe)
except Exception as e:
logger.error(str(e))
goarch = "amd64" if arch == "64" else "386"
cmd = [
"env",
"GOOS=windows",
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'",
f"-X 'main.Atype={atype}'",
f"-X 'main.Rdp={rdp}'",
f"-X 'main.Ping={ping}'",
f"-X 'main.Power={power}'",
f"-X 'main.DownloadUrl={download_url}'",
f"-X 'main.Token={token}'\"",
"-o",
exe,
]
build_error = False
gen_error = False
gen = [
"env",
"GOOS=windows",
f"GOARCH={goarch}",
go_bin,
"generate",
]
try:
r1 = subprocess.run(
" ".join(gen),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except Exception as e:
gen_error = True
logger.error(str(e))
return Response(
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
)
if r1.returncode != 0:
gen_error = True
if r1.stdout:
logger.error(r1.stdout.decode("utf-8", errors="ignore"))
if r1.stderr:
logger.error(r1.stderr.decode("utf-8", errors="ignore"))
logger.error(f"Go build failed with return code {r1.returncode}")
if gen_error:
return Response(
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
)
try:
r = subprocess.run(
" ".join(cmd),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except Exception as e:
build_error = True
logger.error(str(e))
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
if r.returncode != 0:
build_error = True
if r.stdout:
logger.error(r.stdout.decode("utf-8", errors="ignore"))
if r.stderr:
logger.error(r.stderr.decode("utf-8", errors="ignore"))
logger.error(f"Go build failed with return code {r.returncode}")
if build_error:
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
if settings.DEBUG:
with open(exe, "rb") as f:
response = HttpResponse(
f.read(),
content_type="application/vnd.microsoft.portable-executable",
)
response["Content-Disposition"] = f"inline; filename={file_name}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={file_name}"
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
return response
return generate_installer_exe(
file_name="rmm-installer.exe",
goarch="amd64" if arch == "64" else "386",
inno=inno,
api=request.data["api"],
client_id=client_id,
site_id=site_id,
atype=request.data["agenttype"],
rdp=request.data["rdp"],
ping=request.data["ping"],
power=request.data["power"],
download_url=download_url,
token=token,
)
elif request.data["installMethod"] == "manual":
cmd = [
@@ -647,22 +489,14 @@ def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
mode = request.data["mode"]
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
return notify_error("Only available in agent version greater than 0.9.5")
# attempt a realtime recovery, otherwise fall back to old recovery method
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
if not agent.has_nats:
if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
if agent.recoveryactions.filter(last_run=None).exists():
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
return notify_error(
"A recovery action is currently pending. Please wait for the next agent check-in."
)
@@ -690,10 +524,9 @@ def recover(request):
@api_view(["POST"])
def run_script(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
script = get_object_or_404(Script, pk=request.data["scriptPK"])
output = request.data["output"]
args = request.data["args"]
req_timeout = int(request.data["timeout"]) + 3
AuditLog.audit_script_run(
@@ -703,13 +536,12 @@ def run_script(request):
)
if output == "wait":
r = agent.run_script(scriptpk=script.pk, timeout=req_timeout, wait=True)
r = agent.run_script(
scriptpk=script.pk, args=args, timeout=req_timeout, wait=True
)
return Response(r)
elif output == "email":
if not pyver.parse(agent.version) >= pyver.parse("1.1.12"):
return notify_error("Requires agent version 1.1.12 or greater")
emails = (
[] if request.data["emailmode"] == "default" else request.data["emails"]
)
@@ -718,9 +550,10 @@ def run_script(request):
scriptpk=script.pk,
nats_timeout=req_timeout,
emails=emails,
args=args,
)
else:
agent.run_script(scriptpk=script.pk, timeout=req_timeout)
agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
return Response(f"{script.name} will now be run on {agent.hostname}")
@@ -812,7 +645,7 @@ def bulk(request):
elif request.data["monType"] == "workstations":
q = q.filter(monitoring_type="workstation")
agents: List[int] = [agent.pk for agent in q]
agents: list[int] = [agent.pk for agent in q]
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)

View File

@@ -2,6 +2,5 @@ from django.contrib import admin
from .models import Alert, AlertTemplate
admin.site.register(Alert)
admin.site.register(AlertTemplate)

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1 on 2020-08-15 15:31
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.2 on 2020-10-21 18:15
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,8 +1,8 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
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):

View File

@@ -1,7 +1,20 @@
from django.db import models
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from django.conf import settings
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 loguru import logger
if TYPE_CHECKING:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
logger.configure(**settings.LOG_CONFIG)
SEVERITY_CHOICES = [
("info", "Informational"),
@@ -78,18 +91,20 @@ class Alert(models.Model):
self.save()
@classmethod
def create_availability_alert(cls, agent):
def create_or_return_availability_alert(cls, agent):
if not cls.objects.filter(agent=agent, resolved=False).exists():
return cls.objects.create(
agent=agent,
alert_type="availability",
severity="error",
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is Offline.",
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
hidden=True,
)
else:
return cls.objects.get(agent=agent, resolved=False)
@classmethod
def create_check_alert(cls, check):
def create_or_return_check_alert(cls, check):
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
return cls.objects.create(
@@ -99,9 +114,11 @@ class Alert(models.Model):
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
)
else:
return cls.objects.get(assigned_check=check, resolved=False)
@classmethod
def create_task_alert(cls, task):
def create_or_return_task_alert(cls, task):
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
return cls.objects.create(
@@ -111,10 +128,305 @@ class Alert(models.Model):
message=f"{task.agent.hostname} has task: {task.name} that failed.",
hidden=True,
)
else:
return cls.objects.get(assigned_task=task, resolved=False)
@classmethod
def create_custom_alert(cls, custom):
pass
def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
# set variables
dashboard_severities = None
email_severities = None
text_severities = None
always_dashboard = None
always_email = None
always_text = None
alert_interval = None
email_task = None
text_task = None
# check what the instance passed is
if isinstance(instance, Agent):
from agents.tasks import agent_outage_email_task, agent_outage_sms_task
email_task = agent_outage_email_task
text_task = agent_outage_sms_task
email_alert = instance.overdue_email_alert
text_alert = instance.overdue_text_alert
dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode
alert_severity = "error"
agent = instance
# 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
alert_interval = alert_template.agent_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_availability_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(agent=instance, resolved=False).exists():
alert = cls.objects.get(agent=instance, resolved=False)
else:
alert = None
elif isinstance(instance, Check):
from checks.tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
)
email_task = handle_check_email_alert_task
text_task = handle_check_sms_alert_task
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = 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
always_dashboard = alert_template.check_always_alert
always_email = alert_template.check_always_email
always_text = alert_template.check_always_text
alert_interval = alert_template.check_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_check_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_check=instance, resolved=False).exists():
alert = cls.objects.get(assigned_check=instance, resolved=False)
else:
alert = None
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
email_task = handle_task_email_alert
text_task = handle_task_sms_alert
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
# 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
always_dashboard = alert_template.task_always_alert
always_email = alert_template.task_always_email
always_text = alert_template.task_always_text
alert_interval = alert_template.task_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_task_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_task=instance, resolved=False).exists():
alert = cls.objects.get(assigned_task=instance, resolved=False)
else:
alert = None
else:
return
# return if agent is in maintenance mode
if maintenance_mode or not alert:
return
# check if alert severity changed on check and update the alert
if alert_severity != alert.severity:
alert.severity = alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if dashboard_alert or always_dashboard:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in dashboard_severities: # type: ignore
pass
else:
alert.hidden = False
alert.save()
# send email if enabled
if email_alert or always_email:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in email_severities: # type: ignore
pass
else:
email_task.delay(
pk=alert.pk,
alert_interval=alert_interval,
)
# send text if enabled
if text_alert or always_text:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in text_severities: # type: ignore
pass
else:
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
)
@classmethod
def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
# set variables
email_on_resolved = False
text_on_resolved = False
resolved_email_task = None
resolved_text_task = None
# check what the instance passed is
if isinstance(instance, Agent):
from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
resolved_email_task = agent_recovery_email_task
resolved_text_task = agent_recovery_sms_task
alert_template = instance.alert_template
alert = cls.objects.get(agent=instance, resolved=False)
maintenance_mode = instance.maintenance_mode
agent = instance
if alert_template:
email_on_resolved = alert_template.agent_email_on_resolved
text_on_resolved = alert_template.agent_text_on_resolved
elif isinstance(instance, Check):
from checks.tasks import (
handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task,
)
resolved_email_task = handle_resolved_check_email_alert_task
resolved_text_task = handle_resolved_check_sms_alert_task
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_check=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
if alert_template:
email_on_resolved = alert_template.check_email_on_resolved
text_on_resolved = alert_template.check_text_on_resolved
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import (
handle_resolved_task_email_alert,
handle_resolved_task_sms_alert,
)
resolved_email_task = handle_resolved_task_email_alert
resolved_text_task = handle_resolved_task_sms_alert
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_task=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
if alert_template:
email_on_resolved = alert_template.task_email_on_resolved
text_on_resolved = alert_template.task_text_on_resolved
else:
return
# return if agent is in maintenance mode
if maintenance_mode:
return
alert.resolve()
# check if a resolved email notification should be send
if email_on_resolved and not alert.resolved_email_sent:
resolved_email_task.delay(pk=alert.pk)
# check if resolved text should be sent
if text_on_resolved and not alert.resolved_sms_sent:
resolved_text_task.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
)
class AlertTemplate(models.Model):
@@ -243,7 +555,6 @@ class AlertTemplate(models.Model):
return (
self.agent_email_on_resolved
or self.agent_text_on_resolved
or self.agent_include_desktops
or self.agent_always_email
or self.agent_always_text
or self.agent_always_alert
@@ -284,4 +595,4 @@ class AlertTemplate(models.Model):
@property
def is_default_template(self) -> bool:
return self.default_alert_template.exists()
return self.default_alert_template.exists() # type: ignore

View File

@@ -1,13 +1,10 @@
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
)
from rest_framework.serializers import ModelSerializer, ReadOnlyField
from clients.serializers import ClientSerializer, SiteSerializer
from automation.serializers import PolicySerializer
from clients.serializers import ClientSerializer, SiteSerializer
from tacticalrmm.utils import get_default_timezone
from .models import Alert, AlertTemplate

View File

@@ -1,8 +1,7 @@
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
from alerts.models import Alert
from tacticalrmm.celery import app
@app.task
@@ -13,3 +12,13 @@ def unsnooze_alerts() -> str:
)
return "ok"
@app.task
def cache_agents_alert_template():
from agents.models import Agent
for agent in Agent.objects.only("pk"):
agent.set_alert_template()
return "ok"

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -1,19 +1,20 @@
from django.shortcuts import get_object_or_404
from django.db.models import Q
from datetime import datetime as dt
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Alert, AlertTemplate
from .serializers import (
AlertSerializer,
AlertTemplateSerializer,
AlertTemplateRelationSerializer,
AlertTemplateSerializer,
)
from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(AlertTemplate, pk=pk).delete()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")

View File

@@ -1,11 +1,12 @@
import os
import json
import os
from unittest.mock import patch
from django.conf import settings
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from django.utils import timezone as djangotime
from model_bakery import baker
from itertools import cycle
from tacticalrmm.test import TacticalTestCase
class TestAPIv3(TacticalTestCase):
@@ -17,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
def test_get_checks(self):
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
# add a check
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
# override check run interval
check2 = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=20
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
# Set last_run on both checks and should return an empty list
check1.last_run = djangotime.now()
check1.save()
check2.last_run = djangotime.now()
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertFalse(r.data["checks"]) # type: ignore
# set last_run greater than interval
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check1.save()
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
r = self.client.get(url)
@@ -52,3 +89,100 @@ class TestAPIv3(TacticalTestCase):
r.json(),
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)
# add check to agent with check interval set
check = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=30
)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 30},
)
# minimum check run interval is 15 seconds
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 15},
)
def test_checkin_patch(self):
from logs.models import PendingAction
url = "/api/v3/checkin/"
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
PendingAction.objects.create(
agent=agent_updated,
action_type="agentupdate",
details={
"url": agent_updated.winagent_dl,
"version": agent_updated.version,
"inno": agent_updated.win_inno_exe,
},
)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent failed to update and still on same version
payload = {
"func": "hello",
"agent_id": agent_updated.agent_id,
"version": "1.3.0",
}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent successful update
payload["version"] = settings.LATEST_AGENT_VER
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "completed")
action.delete()
@patch("apiv3.views.reload_nats")
def test_agent_recovery(self, reload_nats):
reload_nats.return_value = "ok"
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
self.assertEqual(r.status_code, 404)
agent = baker.make_recipe("agents.online_agent")
url = f"/api/v3/{agent.agent_id}/recovery/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make(
"agents.RecoveryAction",
agent=agent,
mode="command",
command="shutdown /r /t 5 /f",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
)
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [
@@ -16,4 +17,6 @@ urlpatterns = [
path("choco/", views.Choco.as_view()),
path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
]

View File

@@ -1,32 +1,31 @@
import asyncio
import os
import time
from loguru import logger
from packaging import version as pyver
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from django.http import HttpResponse
from loguru import logger
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 rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.authtoken.models import Token
from agents.models import Agent
from checks.models import Check
from checks.utils import bytes2human
from autotasks.models import AutomatedTask
from accounts.models import User
from winupdate.models import WinUpdate, WinUpdatePolicy
from software.models import InstalledSoftware
from checks.serializers import CheckRunnerGetSerializer
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from agents.models import Agent
from agents.serializers import WinAgentSerializer
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializer
from checks.utils import bytes2human
from logs.models import PendingAction
from software.models import InstalledSoftware
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
from winupdate.models import WinUpdate, WinUpdatePolicy
logger.configure(**settings.LOG_CONFIG)
@@ -37,6 +36,8 @@ class CheckIn(APIView):
permission_classes = [IsAuthenticated]
def patch(self, request):
from alerts.models import Alert
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
@@ -52,26 +53,20 @@ class CheckIn(APIView):
# change agent update pending status to completed if agent has just updated
if (
updated
and agent.pendingactions.filter(
and agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter(
agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).update(status="completed")
# handles any alerting actions
agent.handle_alert(checkin=True)
recovery = agent.recoveryactions.filter(last_run=None).last()
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
handle_agent_recovery_task.delay(pk=recovery.pk)
return Response("ok")
if Alert.objects.filter(agent=agent, resolved=False).exists():
Alert.handle_alert_resolve(agent)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
agent.handle_pending_actions()
return Response("ok")
@@ -113,7 +108,7 @@ class CheckIn(APIView):
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s = agent.installedsoftware_set.first() # type: ignore
s.software = sw
s.save(update_fields=["software"])
@@ -186,7 +181,7 @@ class WinUpdates(APIView):
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
u = agent.winupdates.filter(guid=request.data["guid"]).last()
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
success: bool = request.data["success"]
if success:
u.result = "success"
@@ -212,8 +207,8 @@ class WinUpdates(APIView):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"]
for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists():
u = agent.winupdates.filter(guid=update["guid"]).last()
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
u.downloaded = update["downloaded"]
u.installed = update["installed"]
u.save(update_fields=["downloaded", "installed"])
@@ -244,7 +239,7 @@ class WinUpdates(APIView):
# more superseded updates cleanup
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
for u in agent.winupdates.filter(
for u in agent.winupdates.filter( # type: ignore
date_installed__isnull=True, result="failed"
).exclude(installed=True):
u.delete()
@@ -258,7 +253,7 @@ class SupersededWinUpdate(APIView):
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = agent.winupdates.filter(guid=request.data["guid"])
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
for u in updates:
u.delete()
@@ -266,21 +261,37 @@ class SupersededWinUpdate(APIView):
class CheckRunner(APIView):
"""
For the windows golang agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
run_list = [
check
for check in checks
# always run if check hasn't run yet
if not check.last_run
# if a check interval is set, see if the correct amount of seconds have passed
or (
check.run_interval
and (
check.last_run
< djangotime.now()
- djangotime.timedelta(seconds=check.run_interval)
)
# if check interval isn't set, make sure the agent's check interval has passed before running
)
or (
check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
]
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializer(checks, many=True).data,
"check_interval": agent.check_run_interval(),
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
}
return Response(ret)
@@ -299,14 +310,13 @@ class CheckRunnerInterval(APIView):
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
return Response(
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
)
class TaskRunner(APIView):
"""
For the windows golang agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
@@ -316,6 +326,7 @@ class TaskRunner(APIView):
return Response(TaskGOGetSerializer(task).data)
def patch(self, request, pk, agentid):
from alerts.models import Alert
from logs.models import AuditLog
agent = get_object_or_404(Agent, agent_id=agentid)
@@ -327,8 +338,17 @@ class TaskRunner(APIView):
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
new_task = AutomatedTask.objects.get(pk=task.pk)
new_task.handle_alert()
status = "failing" if task.retcode != 0 else "passing"
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
new_task.status = status
new_task.save()
if status == "passing":
if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
Alert.handle_alert_resolve(new_task)
else:
Alert.handle_alert_failure(new_task)
AuditLog.objects.create(
username=agent.hostname,
@@ -406,10 +426,10 @@ class NewAgent(APIView):
agent.salt_id = f"{agent.hostname}-{agent.pk}"
agent.save(update_fields=["salt_id"])
user = User.objects.create_user(
user = User.objects.create_user( # type: ignore
username=request.data["agent_id"],
agent=agent,
password=User.objects.make_random_password(60),
password=User.objects.make_random_password(60), # type: ignore
)
token = Token.objects.create(user=user)
@@ -454,7 +474,7 @@ class Software(APIView):
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s = agent.installedsoftware_set.first() # type: ignore
s.software = sw
s.save(update_fields=["software"])
@@ -477,3 +497,59 @@ class Installer(APIView):
)
return Response("ok")
class ChocoResult(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request, pk):
action = get_object_or_404(PendingAction, pk=pk)
results: str = request.data["results"]
software_name = action.details["name"].lower()
success = [
"install",
"of",
software_name,
"was",
"successful",
"installed",
]
duplicate = [software_name, "already", "installed", "--force", "reinstall"]
installed = False
if all(x in results.lower() for x in success):
installed = True
elif all(x in results.lower() for x in duplicate):
installed = True
action.details["output"] = results
action.details["installed"] = installed
action.status = "completed"
action.save(update_fields=["details", "status"])
return Response("ok")
class AgentRecovery(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
ret = {"mode": "pass", "shellcmd": ""}
if recovery is None:
return Response(ret)
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
ret["mode"] = recovery.mode
if recovery.mode == "command":
ret["shellcmd"] = recovery.command
elif recovery.mode == "rpc":
reload_nats()
return Response(ret)

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.0.6 on 2020-06-04 17:13
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.7 on 2021-03-02 04:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0030_agent_offline_time'),
('clients', '0009_auto_20210212_1408'),
('automation', '0007_policy_alert_template'),
]
operations = [
migrations.AddField(
model_name='policy',
name='excluded_agents',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
),
migrations.AddField(
model_name='policy',
name='excluded_clients',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
),
migrations.AddField(
model_name='policy',
name='excluded_sites',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
),
]

View File

@@ -1,4 +1,5 @@
from django.db import models
from agents.models import Agent
from core.models import CoreSettings
from logs.models import BaseAuditModel
@@ -16,8 +17,18 @@ class Policy(BaseAuditModel):
null=True,
blank=True,
)
excluded_sites = models.ManyToManyField(
"clients.Site", related_name="policy_exclusions", blank=True
)
excluded_clients = models.ManyToManyField(
"clients.Client", related_name="policy_exclusions", blank=True
)
excluded_agents = models.ManyToManyField(
"agents.Agent", related_name="policy_exclusions", blank=True
)
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_from_policies_task
# get old policy if exists
@@ -32,6 +43,9 @@ class Policy(BaseAuditModel):
create_tasks=True,
)
if old_policy.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
@@ -42,28 +56,50 @@ class Policy(BaseAuditModel):
@property
def is_default_server_policy(self):
return self.default_server_policy.exists()
return self.default_server_policy.exists() # type: ignore
@property
def is_default_workstation_policy(self):
return self.default_workstation_policy.exists()
return self.default_workstation_policy.exists() # type: ignore
def __str__(self):
return self.name
def is_agent_excluded(self, agent):
return (
agent in self.excluded_agents.all()
or agent.site in self.excluded_sites.all()
or agent.client in self.excluded_clients.all()
)
def related_agents(self):
return self.get_related("server") | self.get_related("workstation")
def get_related(self, mon_type):
explicit_agents = self.agents.filter(monitoring_type=mon_type)
explicit_clients = getattr(self, f"{mon_type}_clients").all()
explicit_sites = getattr(self, f"{mon_type}_sites").all()
explicit_agents = (
self.agents.filter(monitoring_type=mon_type) # type: ignore
.exclude(
pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
)
.exclude(site__in=self.excluded_sites.all())
.exclude(site__client__in=self.excluded_clients.all())
)
explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
pk__in=self.excluded_clients.all()
)
explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
pk__in=self.excluded_sites.all()
)
filtered_agents_pks = Policy.objects.none()
filtered_agents_pks |= Agent.objects.filter(
site__in=[
site for site in explicit_sites if site.client not in explicit_clients
site
for site in explicit_sites
if site.client not in explicit_clients
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
).values_list("pk", flat=True)
@@ -87,9 +123,8 @@ class Policy(BaseAuditModel):
@staticmethod
def cascade_policy_tasks(agent):
from autotasks.tasks import delete_win_task_schedule
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
from logs.models import PendingAction
# List of all tasks to be applied
@@ -119,23 +154,39 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
if agent_policy and agent_policy.active:
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
for task in agent_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if site_policy and site_policy.active:
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
for task in site_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if client_policy and client_policy.active:
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
for task in client_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if default_policy and default_policy.active:
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
for task in default_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
@@ -205,7 +256,11 @@ class Policy(BaseAuditModel):
enforced_checks = list()
policy_checks = list()
if agent_policy and agent_policy.active:
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
if agent_policy.enforced:
for check in agent_policy.policychecks.all():
enforced_checks.append(check)
@@ -213,7 +268,11 @@ class Policy(BaseAuditModel):
for check in agent_policy.policychecks.all():
policy_checks.append(check)
if site_policy and site_policy.active:
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
if site_policy.enforced:
for check in site_policy.policychecks.all():
enforced_checks.append(check)
@@ -221,7 +280,11 @@ class Policy(BaseAuditModel):
for check in site_policy.policychecks.all():
policy_checks.append(check)
if client_policy and client_policy.active:
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
if client_policy.enforced:
for check in client_policy.policychecks.all():
enforced_checks.append(check)
@@ -229,7 +292,11 @@ class Policy(BaseAuditModel):
for check in client_policy.policychecks.all():
policy_checks.append(check)
if default_policy and default_policy.active:
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
if default_policy.enforced:
for check in default_policy.policychecks.all():
enforced_checks.append(check)

View File

@@ -1,16 +1,18 @@
from django.db.models.base import Model
from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
ReadOnlyField,
SerializerMethodField,
)
from .models import Policy
from agents.serializers import AgentHostnameSerializer
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
class PolicySerializer(ModelSerializer):
class Meta:
@@ -25,6 +27,9 @@ class PolicyTableSerializer(ModelSerializer):
agents_count = SerializerMethodField(read_only=True)
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
alert_template = ReadOnlyField(source="alert_template.id")
excluded_clients = ClientSerializer(many=True)
excluded_sites = SiteSerializer(many=True)
excluded_agents = AgentHostnameSerializer(many=True)
class Meta:
model = Policy

View File

@@ -1,8 +1,7 @@
from agents.models import Agent
from automation.models import Policy
from autotasks.models import AutomatedTask
from checks.models import Check
from agents.models import Agent
from tacticalrmm.celery import app
@@ -80,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
error_threshold=check.error_threshold,
alert_severity=check.alert_severity,
name=check.name,
run_interval=check.run_interval,
disk=check.disk,
fails_b4_alert=check.fails_b4_alert,
ip=check.ip,
@@ -99,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
event_message=check.event_message,
fail_when=check.fail_when,
search_last_days=check.search_last_days,
number_of_events_b4_alert=check.number_of_events_b4_alert,
email_alert=check.email_alert,
text_alert=check.text_alert,
dashboard_alert=check.dashboard_alert,
@@ -130,8 +131,8 @@ def generate_agent_tasks_from_policies_task(policypk):
@app.task
def delete_policy_autotask_task(taskpk):
from autotasks.tasks import delete_win_task_schedule
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
for task in AutomatedTask.objects.filter(parent_task=taskpk):
delete_win_task_schedule.delay(task.pk)

View File

@@ -1,18 +1,21 @@
from unittest.mock import patch
from tacticalrmm.test import TacticalTestCase
from model_bakery import baker, seq
from itertools import cycle
from unittest.mock import patch
from model_bakery import baker, seq
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from .serializers import (
PolicyTableSerializer,
PolicySerializer,
PolicyTaskStatusSerializer,
PolicyOverviewSerializer,
PolicyCheckStatusSerializer,
PolicyCheckSerializer,
AutoTasksFieldSerializer,
PolicyCheckSerializer,
PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
@@ -29,7 +32,7 @@ class TestPolicyViews(TacticalTestCase):
serializer = PolicyTableSerializer(policies, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -39,13 +42,13 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
policy = baker.make("automation.Policy")
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
resp = self.client.get(url, format="json")
serializer = PolicySerializer(policy)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -77,13 +80,13 @@ class TestPolicyViews(TacticalTestCase):
"desc": "policy desc",
"active": True,
"enforced": False,
"copyId": policy.pk,
"copyId": policy.pk, # type: ignore
}
resp = self.client.post(f"/automation/policies/", data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(policy.autotasks.count(), 3)
self.assertEqual(policy.policychecks.count(), 7)
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
self.check_not_authenticated("post", url)
@@ -94,7 +97,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
policy = baker.make("automation.Policy", active=True, enforced=False)
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
data = {
"name": "Test Policy Update",
@@ -119,7 +122,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_from_policies_task.assert_called_with(
policypk=policy.pk, create_tasks=True
policypk=policy.pk, create_tasks=True # type: ignore
)
self.check_not_authenticated("put", url)
@@ -136,7 +139,7 @@ class TestPolicyViews(TacticalTestCase):
agents = baker.make_recipe(
"agents.agent", site=site, policy=policy, _quantity=3
)
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -151,14 +154,14 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks
policy = baker.make("automation.Policy")
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/"
url = f"/automation/{policy.pk}/policyautomatedtasks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AutoTasksFieldSerializer(tasks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 3)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 3) # type: ignore
self.check_not_authenticated("get", url)
@@ -168,14 +171,14 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
checks = self.create_checks(policy=policy)
url = f"/automation/{policy.pk}/policychecks/"
url = f"/automation/{policy.pk}/policychecks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = PolicyCheckSerializer(checks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 7)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 7) # type: ignore
self.check_not_authenticated("get", url)
@@ -197,7 +200,7 @@ class TestPolicyViews(TacticalTestCase):
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("patch", url)
def test_policy_overview(self):
@@ -210,40 +213,40 @@ class TestPolicyViews(TacticalTestCase):
)
clients = baker.make(
"clients.Client",
server_policy=cycle(policies),
workstation_policy=cycle(policies),
server_policy=cycle(policies), # type: ignore
workstation_policy=cycle(policies), # type: ignore
_quantity=5,
)
baker.make(
"clients.Site",
client=cycle(clients),
server_policy=cycle(policies),
workstation_policy=cycle(policies),
client=cycle(clients), # type: ignore
server_policy=cycle(policies), # type: ignore
workstation_policy=cycle(policies), # type: ignore
_quantity=4,
)
baker.make("clients.Site", client=cycle(clients), _quantity=3)
baker.make("clients.Site", client=cycle(clients), _quantity=3) # type: ignore
resp = self.client.get(url, format="json")
clients = Client.objects.all()
serializer = PolicyOverviewSerializer(clients, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_get_related(self):
policy = baker.make("automation.Policy")
url = f"/automation/policies/{policy.pk}/related/"
url = f"/automation/policies/{policy.pk}/related/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertIsInstance(resp.data["server_clients"], list)
self.assertIsInstance(resp.data["workstation_clients"], list)
self.assertIsInstance(resp.data["server_sites"], list)
self.assertIsInstance(resp.data["workstation_sites"], list)
self.assertIsInstance(resp.data["agents"], list)
self.assertIsInstance(resp.data["server_clients"], list) # type: ignore
self.assertIsInstance(resp.data["workstation_clients"], list) # type: ignore
self.assertIsInstance(resp.data["server_sites"], list) # type: ignore
self.assertIsInstance(resp.data["workstation_sites"], list) # type: ignore
self.assertIsInstance(resp.data["agents"], list) # type: ignore
self.check_not_authenticated("get", url)
@@ -255,16 +258,16 @@ class TestPolicyViews(TacticalTestCase):
# create policy managed tasks
policy_tasks = baker.make(
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
)
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
resp = self.client.patch(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 5)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 5) # type: ignore
self.check_not_authenticated("patch", url)
@@ -282,7 +285,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_task.assert_called_once_with([task.pk for task in tasks])
mock_task.assert_called_once_with([task.pk for task in tasks]) # type: ignore
self.check_not_authenticated("put", url)
@@ -297,7 +300,7 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
data = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"critical": "approve",
"important": "approve",
"moderate": "ignore",
@@ -323,11 +326,11 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
url = f"/automation/winupdatepolicy/{patch_policy.pk}/"
url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore
data = {
"id": patch_policy.pk,
"policy": policy.pk,
"id": patch_policy.pk, # type: ignore
"policy": policy.pk, # type: ignore
"critical": "approve",
"important": "approve",
"moderate": "ignore",
@@ -356,10 +359,10 @@ class TestPolicyViews(TacticalTestCase):
}
clients = baker.make("clients.Client", _quantity=6)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10) # type: ignore
agents = baker.make_recipe(
"agents.agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=6,
)
@@ -369,24 +372,24 @@ class TestPolicyViews(TacticalTestCase):
)
# test reset agents in site
data = {"site": sites[0].id}
data = {"site": sites[0].id} # type: ignore
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site=sites[0])
agents = Agent.objects.filter(site=sites[0]) # type: ignore
for agent in agents:
for k, v in inherit_fields.items():
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
# test reset agents in client
data = {"client": clients[1].id}
data = {"client": clients[1].id} # type: ignore
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site__client=clients[1])
agents = Agent.objects.filter(site__client=clients[1]) # type: ignore
for agent in agents:
for k, v in inherit_fields.items():
@@ -423,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_sync_policy(self, generate_checks):
url = "/automation/sync/"
# test invalid data
data = {"invalid": 7}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
policy = baker.make("automation.Policy", active=True)
data = {"policy": policy.pk} # type: ignore
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
self.check_not_authenticated("post", url)
class TestPolicyTasks(TacticalTestCase):
def setUp(self):
@@ -433,46 +455,46 @@ class TestPolicyTasks(TacticalTestCase):
# Get Site and Client from an agent in list
clients = baker.make("clients.Client", _quantity=5)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25) # type: ignore
server_agents = baker.make_recipe(
"agents.server_agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=25,
)
workstation_agents = baker.make_recipe(
"agents.workstation_agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=25,
)
policy = baker.make("automation.Policy", active=True)
# Add Client to Policy
policy.server_clients.add(server_agents[13].client)
policy.workstation_clients.add(workstation_agents[15].client)
policy.server_clients.add(server_agents[13].client) # type: ignore
policy.workstation_clients.add(workstation_agents[15].client) # type: ignore
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json"
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
)
self.assertEqual(resp.status_code, 200)
self.assertEquals(len(resp.data["server_clients"]), 1)
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_clients"]), 1)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 10)
self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Site to Policy and the agents and sites length shouldn't change
policy.server_sites.add(server_agents[13].site)
policy.workstation_sites.add(workstation_agents[15].site)
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 10)
policy.server_sites.add(server_agents[13].site) # type: ignore
policy.workstation_sites.add(workstation_agents[15].site) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Agent to Policy and the agents length shouldn't change
policy.agents.add(server_agents[13])
policy.agents.add(workstation_agents[15])
self.assertEquals(len(resp.data["agents"]), 10)
policy.agents.add(server_agents[13]) # type: ignore
policy.agents.add(workstation_agents[15]) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
def test_generating_agent_policy_checks(self):
from .tasks import generate_agent_checks_from_policies_task
@@ -483,7 +505,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id)
generate_agent_checks_from_policies_task(policy.id) # type: ignore
# make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -503,12 +525,12 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(check.ip, checks[1].ip)
elif check.check_type == "cpuload":
self.assertEqual(check.parent_check, checks[2].id)
self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
self.assertEqual(check.error_threshold, checks[2].error_threshold)
self.assertEqual(check.warning_threshold, checks[2].warning_threshold)
elif check.check_type == "memory":
self.assertEqual(check.parent_check, checks[3].id)
self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
self.assertEqual(check.error_threshold, checks[3].error_threshold)
self.assertEqual(check.warning_threshold, checks[3].warning_threshold)
elif check.check_type == "winsvc":
self.assertEqual(check.parent_check, checks[4].id)
self.assertEqual(check.svc_name, checks[4].svc_name)
@@ -533,7 +555,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
self.create_checks(agent=agent, script=script)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True) # type: ignore
# make sure each agent check says overriden_by_policy
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
@@ -765,9 +787,10 @@ class TestPolicyTasks(TacticalTestCase):
def test_generating_policy_checks_for_all_agents(
self, generate_all_agent_checks_task
):
from .tasks import generate_all_agent_checks_task as generate_all_checks
from core.models import CoreSettings
from .tasks import generate_all_agent_checks_task as generate_all_checks
# setup data
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
@@ -829,8 +852,8 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
def test_delete_policy_check(self):
from .tasks import delete_policy_check_task
from .models import Policy
from .tasks import delete_policy_check_task
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
@@ -840,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
# pick a policy check and delete it from the agent
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
delete_policy_check_task(policy_check_id)
@@ -853,8 +876,8 @@ class TestPolicyTasks(TacticalTestCase):
)
def update_policy_check_fields(self):
from .tasks import update_policy_check_fields_task
from .models import Policy
from .tasks import update_policy_check_fields_task
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
@@ -865,7 +888,7 @@ class TestPolicyTasks(TacticalTestCase):
# pick a policy check and update it with new values
ping_check = (
Policy.objects.get(pk=policy.id)
Policy.objects.get(pk=policy.id) # type: ignore
.policychecks.filter(check_type="ping")
.first()
)
@@ -892,7 +915,7 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_tasks_from_policies_task(policy.id)
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -902,14 +925,14 @@ class TestPolicyTasks(TacticalTestCase):
for task in agent_tasks:
self.assertTrue(task.managed_by_policy)
if task.name == "Task1":
self.assertEqual(task.parent_task, tasks[0].id)
self.assertEqual(task.name, tasks[0].name)
self.assertEqual(task.parent_task, tasks[0].id) # type: ignore
self.assertEqual(task.name, tasks[0].name) # type: ignore
if task.name == "Task2":
self.assertEqual(task.parent_task, tasks[1].id)
self.assertEqual(task.name, tasks[1].name)
self.assertEqual(task.parent_task, tasks[1].id) # type: ignore
self.assertEqual(task.name, tasks[1].name) # type: ignore
if task.name == "Task3":
self.assertEqual(task.parent_task, tasks[2].id)
self.assertEqual(task.name, tasks[2].name)
self.assertEqual(task.parent_task, tasks[2].id) # type: ignore
self.assertEqual(task.name, tasks[2].name) # type: ignore
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_delete_policy_tasks(self, delete_win_task_schedule):
@@ -919,10 +942,10 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
delete_policy_autotask_task(tasks[0].id)
delete_policy_autotask_task(tasks[0].id) # type: ignore
delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
)
@patch("autotasks.tasks.run_win_task.delay")
@@ -931,12 +954,12 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
run_win_policy_autotask_task([task.id for task in tasks])
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
run_win_task.side_effect = [task.id for task in tasks]
run_win_task.side_effect = [task.id for task in tasks] # type: ignore
self.assertEqual(run_win_task.call_count, 3)
for task in tasks:
run_win_task.assert_any_call(task.id)
for task in tasks: # type: ignore
run_win_task.assert_any_call(task.id) # type: ignore
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, enable_or_disable_win_task):
@@ -949,17 +972,17 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
tasks[0].enabled = False
tasks[0].save()
tasks[0].enabled = False # type: ignore
tasks[0].save() # type: ignore
update_policy_task_fields_task(tasks[0].id)
update_policy_task_fields_task(tasks[0].id) # type: ignore
enable_or_disable_win_task.assert_not_called()
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
update_policy_task_fields_task(tasks[0].id, update_agent=True)
update_policy_task_fields_task(tasks[0].id, update_agent=True) # type: ignore
enable_or_disable_win_task.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id, False
agent.autotasks.get(parent_task=tasks[0].id).id, False # type: ignore
)
@patch("agents.models.Agent.generate_tasks_from_policies")
@@ -981,3 +1004,110 @@ class TestPolicyTasks(TacticalTestCase):
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
self.assertEquals(generate_checks.call_count, 5)
self.assertEquals(generate_checks.call_count, 5)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_policy_exclusions(self, delete_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
task = baker.make("autotasks.AutomatedTask", policy=policy)
agent = baker.make_recipe(
"agents.agent", policy=policy, monitoring_type="server"
)
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add agent to policy exclusions
policy.excluded_agents.set([agent]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks
agent.autotasks.all().delete()
policy.excluded_agents.clear() # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to site
policy.excluded_sites.set([agent.site]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
policy.excluded_sites.clear() # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to client
policy.excluded_clients.set([agent.client]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
policy.excluded_clients.clear() # type: ignore
agent.policy = None
agent.save()
# test on default policy
core = CoreSettings.objects.first()
core.server_policy = policy
core.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to client
policy.excluded_clients.set([agent.client]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
def test_removing_duplicate_pending_task_actions(self):
pass
def test_creating_checks_with_assigned_tasks(self):
pass

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [
@@ -6,6 +7,7 @@ urlpatterns = [
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("sync/", views.PolicySync.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),

View File

@@ -1,32 +1,28 @@
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Policy
from agents.models import Agent
from clients.models import Client
from checks.models import Check
from autotasks.models import AutomatedTask
from winupdate.models import WinUpdatePolicy
from clients.serializers import ClientSerializer, SiteSerializer
from agents.serializers import AgentHostnameSerializer
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from tacticalrmm.utils import notify_error
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
from .serializers import (
AutoTasksFieldSerializer,
PolicyCheckSerializer,
PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
PolicyOverviewSerializer,
PolicyCheckStatusSerializer,
PolicyCheckSerializer,
PolicyTaskStatusSerializer,
AutoTasksFieldSerializer,
)
from .tasks import (
run_win_policy_autotask_task,
)
from .tasks import run_win_policy_autotask_task
class GetAddPolicies(APIView):
@@ -77,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
return Response("ok")
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_from_policies_task
generate_agent_checks_from_policies_task.delay(
request.data["policy"], create_tasks=True
)
return Response("ok")
else:
return notify_error("The request was invalid")
class PolicyAutoTask(APIView):
# tasks associated with policy
@@ -176,7 +186,7 @@ class UpdatePatchPolicy(APIView):
serializer = WinUpdatePolicySerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.policy = policy
serializer.policy = policy # type: ignore
serializer.save()
return Response("ok")

View File

@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks

View File

@@ -1,8 +1,8 @@
# Generated by Django 3.0.6 on 2020-05-31 01:23
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):

View File

@@ -1,4 +1,5 @@
from django.db import migrations
from tacticalrmm.utils import get_bit_days
DAYS_OF_WEEK = {

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-02-24 05:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0017_auto_20210210_1512'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='run_asap_after_missed',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,19 +1,17 @@
import pytz
import datetime as dt
import random
import string
import datetime as dt
from django.utils import timezone as djangotime
import pytz
from django.conf import settings
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import DateTimeField
from logs.models import BaseAuditModel
from tacticalrmm.utils import bitdays_to_string
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from logs.models import BaseAuditModel
from tacticalrmm.utils import bitdays_to_string
logger.configure(**settings.LOG_CONFIG)
@@ -97,6 +95,7 @@ class AutomatedTask(BaseAuditModel):
)
run_time_date = DateTimeField(null=True, blank=True)
remove_if_not_scheduled = models.BooleanField(default=False)
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
managed_by_policy = models.BooleanField(default=False)
parent_task = models.PositiveIntegerField(null=True, blank=True)
win_task_name = models.CharField(max_length=255, null=True, blank=True)
@@ -165,7 +164,6 @@ class AutomatedTask(BaseAuditModel):
# if policy is present, then this task is being copied to another policy
# if agent is present, then this task is being created on an agent from a policy
# exit if neither are set or if both are set
if not agent and not policy or agent and policy:
return
@@ -220,164 +218,30 @@ class AutomatedTask(BaseAuditModel):
timeout=self.timeout,
enabled=self.enabled,
remove_if_not_scheduled=self.remove_if_not_scheduled,
run_asap_after_missed=self.run_asap_after_missed,
)
create_win_task_schedule.delay(task.pk)
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
from autotasks.tasks import (
handle_task_email_alert,
handle_task_sms_alert,
handle_resolved_task_sms_alert,
handle_resolved_task_email_alert,
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.task_always_alert
or alert_template.task_always_email
or alert_template.task_always_text
)
)
)
self.status = "failing" if self.retcode != 0 else "passing"
self.save()
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_task=self, resolved=False).exists():
alert = Alert.objects.get(assigned_task=self, resolved=False)
alert.resolve()
# check if resolved email should be send
if (
not alert.resolved_email_sent
and self.email_alert
or alert_template
and alert_template.task_email_on_resolved
):
handle_resolved_task_email_alert.delay(pk=alert.pk)
# check if resolved text should be sent
if (
not alert.resolved_sms_sent
and self.text_alert
or alert_template
and alert_template.task_text_on_resolved
):
handle_resolved_task_sms_alert.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
)
# create alert if task is failing
else:
if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
alert = Alert.create_task_alert(self)
else:
alert = Alert.objects.get(assigned_task=self, resolved=False)
# check if alert severity changed on task and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if (
self.dashboard_alert
or alert_template
and alert_template.task_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if (
not alert.email_sent
and self.email_alert
or alert_template
and self.alert_severity in alert_template.task_email_alert_severity
and alert_template.check_always_email
):
handle_task_email_alert.delay(
pk=alert.pk,
alert_template=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# send text if enabled
if (
not alert.sms_sent
and self.text_alert
or alert_template
and self.alert_severity in alert_template.task_text_alert_severity
and alert_template.check_always_text
):
handle_task_sms_alert.delay(
pk=alert.pk,
alert_template=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
)
def send_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -389,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template)
CORE.send_mail(subject, body, self.agent.alert_template)
def send_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -408,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
@@ -422,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)

View File

@@ -1,12 +1,11 @@
import pytz
from rest_framework import serializers
from .models import AutomatedTask
from agents.models import Agent
from scripts.models import Script
from scripts.serializers import ScriptCheckSerializer
from checks.serializers import CheckSerializer
from scripts.models import Script
from scripts.serializers import ScriptCheckSerializer
from .models import AutomatedTask
class TaskSerializer(serializers.ModelSerializer):
@@ -19,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -1,17 +1,19 @@
import asyncio
import datetime as dt
from loguru import logger
from tacticalrmm.celery import app
from django.conf import settings
import pytz
from django.utils import timezone as djangotime
from packaging import version as pyver
from typing import Union
import random
from time import sleep
from typing import Union
import pytz
from django.conf import settings
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from logs.models import PendingAction
from tacticalrmm.celery import app
from .models import AutomatedTask
from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG)
@@ -43,7 +45,7 @@ def create_win_task_schedule(pk, pending_action=False):
task.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
task.save()
task.save(update_fields=["run_time_date"])
nats_data = {
"func": "schedtask",
@@ -60,9 +62,12 @@ def create_win_task_schedule(pk, pending_action=False):
},
}
if task.remove_if_not_scheduled and pyver.parse(
if task.run_asap_after_missed and pyver.parse(
task.agent.version
) >= pyver.parse("1.1.2"):
) >= pyver.parse("1.4.7"):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if task.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif task.task_type == "checkfailure" or task.task_type == "manual":

View File

@@ -1,14 +1,15 @@
import datetime as dt
from unittest.mock import patch, call
from model_bakery import baker
from django.utils import timezone as djangotime
from unittest.mock import call, patch
from django.utils import timezone as djangotime
from model_bakery import baker
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .models import AutomatedTask
from logs.models import PendingAction
from .serializers import AutoTaskSerializer
from .tasks import remove_orphaned_win_tasks, run_win_task, create_win_task_schedule
from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task
class TestAutotaskViews(TacticalTestCase):

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -1,32 +1,28 @@
import asyncio
import pytz
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import AutomatedTask
from agents.models import Agent
from checks.models import Check
from scripts.models import Script
from core.models import CoreSettings
from .serializers import TaskSerializer, AutoTaskSerializer
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
from .models import AutomatedTask
from .serializers import AutoTaskSerializer, TaskSerializer
from .tasks import (
create_win_task_schedule,
delete_win_task_schedule,
enable_or_disable_win_task,
)
from tacticalrmm.utils import notify_error, get_bit_days
class AddAutoTask(APIView):
def post(self, request):
from automation.tasks import generate_agent_tasks_from_policies_task
from automation.models import Policy
from automation.tasks import generate_agent_tasks_from_policies_task
data = request.data
script = get_object_or_404(Script, pk=data["autotask"]["script"])
@@ -76,7 +72,7 @@ class AutoTask(APIView):
agent = get_object_or_404(Agent, pk=pk)
ctx = {
"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone),
"default_tz": get_default_timezone(),
"agent_tz": agent.time_zone,
}
return Response(AutoTaskSerializer(agent, context=ctx).data)

View File

@@ -3,7 +3,7 @@ from model_bakery.recipe import Recipe
check = Recipe("checks.Check")
diskspace_check = check.extend(
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10
)
cpuload_check = check.extend(
@@ -13,7 +13,7 @@ cpuload_check = check.extend(
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
memory_check = check.extend(
check_type="memory", warning_threshold=30, error_threshold=75
check_type="memory", warning_threshold=60, error_threshold=75
)
winsvc_check = check.extend(
@@ -21,6 +21,7 @@ winsvc_check = check.extend(
svc_name="ServiceName",
svc_display_name="ServiceName",
svc_policy_mode="manual",
pass_if_svc_not_exist=False,
)
eventlog_check = check.extend(

View File

@@ -3,8 +3,8 @@
import django.contrib.postgres.fields
import django.contrib.postgres.fields.jsonb
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.4 on 2021-01-09 21:36
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0021_auto_20210212_1429'),
]
operations = [
migrations.AddField(
model_name='check',
name='number_of_events_b4_alert',
field=models.PositiveIntegerField(blank=True, default=1, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0022_check_number_of_events_b4_alert'),
]
operations = [
migrations.AddField(
model_name='check',
name='run_interval',
field=models.PositiveIntegerField(blank=True, default=0),
),
]

View File

@@ -1,31 +1,22 @@
import asyncio
import string
import os
import json
import pytz
import os
import string
from statistics import mean
from typing import Any
from django.utils import timezone as djangotime
from django.db import models
import pytz
from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.postgres.fields import ArrayField
from rest_framework.fields import JSONField
from typing import List, Any
from typing import Union
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from logs.models import BaseAuditModel
from .tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task,
)
from .utils import bytes2human
from alerts.models import SEVERITY_CHOICES
logger.configure(**settings.LOG_CONFIG)
@@ -102,6 +93,7 @@ class Check(BaseAuditModel):
fail_count = models.PositiveIntegerField(default=0)
outage_history = models.JSONField(null=True, blank=True) # store
extra_details = models.JSONField(null=True, blank=True)
run_interval = models.PositiveIntegerField(blank=True, default=0)
# check specific fields
# for eventlog, script, ip, and service alert severity
@@ -190,6 +182,9 @@ class Check(BaseAuditModel):
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
)
search_last_days = models.PositiveIntegerField(null=True, blank=True)
number_of_events_b4_alert = models.PositiveIntegerField(
null=True, blank=True, default=1
)
def __str__(self):
if self.agent:
@@ -207,9 +202,9 @@ class Check(BaseAuditModel):
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
return f"{self.get_check_type_display()}: Drive {self.disk} - {text}" # type: ignore
elif self.check_type == "ping":
return f"{self.get_check_type_display()}: {self.name}"
return f"{self.get_check_type_display()}: {self.name}" # type: ignore
elif self.check_type == "cpuload" or self.check_type == "memory":
text = ""
@@ -218,13 +213,13 @@ class Check(BaseAuditModel):
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()} > {text}"
return f"{self.get_check_type_display()} - {text}" # type: ignore
elif self.check_type == "winsvc":
return f"{self.get_check_type_display()}: {self.svc_display_name}"
return f"{self.get_check_type_display()}: {self.svc_display_name}" # type: ignore
elif self.check_type == "eventlog":
return f"{self.get_check_type_display()}: {self.name}"
return f"{self.get_check_type_display()}: {self.name}" # type: ignore
elif self.check_type == "script":
return f"{self.get_check_type_display()}: {self.script.name}"
return f"{self.get_check_type_display()}: {self.script.name}" # type: ignore
else:
return "n/a"
@@ -243,7 +238,7 @@ class Check(BaseAuditModel):
return self.last_run
@property
def non_editable_fields(self) -> List[str]:
def non_editable_fields(self) -> list[str]:
return [
"check_type",
"status",
@@ -268,147 +263,27 @@ class Check(BaseAuditModel):
"modified_time",
]
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
def should_create_alert(self, alert_template=None):
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
alert = Alert.objects.get(assigned_check=self, resolved=False)
alert.resolve()
# check if a resolved email notification should be send
if (
alert_template
and alert_template.check_email_on_resolved
and not alert.resolved_email_sent
):
handle_resolved_check_email_alert_task.delay(pk=alert.pk)
# check if resolved text should be sent
if (
alert_template
and alert_template.check_text_on_resolved
and not alert.resolved_sms_sent
):
handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
)
elif self.fail_count >= self.fails_b4_alert:
if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
alert = Alert.create_check_alert(self)
else:
alert = Alert.objects.get(assigned_check=self, resolved=False)
# check if alert severity changed on check and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if (
self.dashboard_alert
or alert_template
and self.alert_severity in alert_template.check_dashboard_alert_severity
and alert_template.check_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if (
not alert.email_sent
and self.email_alert
or alert_template
and self.alert_severity in alert_template.check_email_alert_severity
and alert_template.check_always_email
):
handle_check_email_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
return (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.check_always_alert
or alert_template.check_always_email
or alert_template.check_always_text
)
# send text if enabled
if (
not alert.sms_sent
and self.text_alert
or alert_template
and self.alert_severity in alert_template.check_text_alert_severity
and alert_template.check_always_text
):
handle_check_sms_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
)
)
)
def add_check_history(self, value: int, more_info: Any = None) -> None:
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
def handle_checkv2(self, data):
from alerts.models import Alert
# cpuload or mem checks
if self.check_type == "cpuload" or self.check_type == "memory":
@@ -617,13 +492,13 @@ class Check(BaseAuditModel):
log.append(i)
if self.fail_when == "contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "failing"
else:
self.status = "passing"
elif self.fail_when == "not_contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "passing"
else:
self.status = "failing"
@@ -641,11 +516,14 @@ class Check(BaseAuditModel):
self.fail_count += 1
self.save(update_fields=["status", "fail_count", "alert_severity"])
if self.fail_count >= self.fails_b4_alert:
Alert.handle_alert_failure(self)
elif self.status == "passing":
self.fail_count = 0
self.save(update_fields=["status", "fail_count", "alert_severity"])
self.handle_alert()
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
Alert.handle_alert_resolve(self)
return self.status
@@ -689,6 +567,7 @@ class Check(BaseAuditModel):
text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details,
run_interval=self.run_interval,
error_threshold=self.error_threshold,
warning_threshold=self.warning_threshold,
disk=self.disk,
@@ -712,6 +591,7 @@ class Check(BaseAuditModel):
event_message=self.event_message,
fail_when=self.fail_when,
search_last_days=self.search_last_days,
number_of_events_b4_alert=self.number_of_events_b4_alert,
)
def is_duplicate(self, check):
@@ -739,11 +619,10 @@ class Check(BaseAuditModel):
def send_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
else:
subject = f"{self} Failed"
@@ -821,12 +700,11 @@ class Check(BaseAuditModel):
except:
continue
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
@@ -870,21 +748,21 @@ class Check(BaseAuditModel):
elif self.check_type == "eventlog":
body = subject
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = f"{self} is now back to normal"
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
CORE.send_sms(subject, alert_template=alert_template)
CORE.send_sms(subject, alert_template=self.agent.alert_template)
class CheckHistory(models.Model):

View File

@@ -1,10 +1,11 @@
import validators as _v
import pytz
import validators as _v
from rest_framework import serializers
from .models import Check, CheckHistory
from autotasks.models import AutomatedTask
from scripts.serializers import ScriptSerializer, ScriptCheckSerializer
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
from .models import Check, CheckHistory
class AssignedTaskField(serializers.ModelSerializer):
@@ -24,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -3,9 +3,10 @@ import random
from time import sleep
from typing import Union
from tacticalrmm.celery import app
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
@app.task
def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str:

View File

@@ -1,11 +1,13 @@
from checks.models import CheckHistory
from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer
from django.utils import timezone as djangotime
from unittest.mock import patch
from django.utils import timezone as djangotime
from model_bakery import baker
from checks.models import CheckHistory
from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer
class TestCheckViews(TacticalTestCase):
def setUp(self):
@@ -22,7 +24,7 @@ class TestCheckViews(TacticalTestCase):
serializer = CheckSerializer(disk_check)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_disk_check(self):
@@ -209,7 +211,7 @@ class TestCheckViews(TacticalTestCase):
serializer = CheckSerializer(disk_check)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("post", url)
def test_add_policy_disk_check(self):
@@ -219,7 +221,7 @@ class TestCheckViews(TacticalTestCase):
url = "/checks/checks/"
valid_payload = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"disk": "M:",
@@ -231,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
# should fail because both error and warning thresholds are 0
invalid_payload = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"error_threshold": 0,
@@ -245,7 +247,7 @@ class TestCheckViews(TacticalTestCase):
# should fail because warning is less than error
invalid_payload = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"error_threshold": 80,
@@ -259,7 +261,7 @@ class TestCheckViews(TacticalTestCase):
# this should fail because we already have a check for drive M: in setup
invalid_payload = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"disk": "M:",
@@ -275,8 +277,8 @@ class TestCheckViews(TacticalTestCase):
def test_get_disks_for_policies(self):
url = "/checks/getalldisks/"
r = self.client.get(url)
self.assertIsInstance(r.data, list)
self.assertEqual(26, len(r.data))
self.assertIsInstance(r.data, list) # type: ignore
self.assertEqual(26, len(r.data)) # type: ignore
def test_edit_check_alert(self):
# setup data
@@ -359,8 +361,8 @@ class TestCheckViews(TacticalTestCase):
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
for check_history in check_history_data: # type: ignore
check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore
check_history.save()
# test invalid check pk
@@ -373,20 +375,22 @@ class TestCheckViews(TacticalTestCase):
data = {"timeFilter": 30}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 30)
self.assertEqual(len(resp.data), 30) # type: ignore
# test with timeFilter equal to 0
data = {"timeFilter": 0}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 60)
self.assertEqual(len(resp.data), 60) # type: ignore
self.check_not_authenticated("patch", url)
class TestCheckTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
self.agent = baker.make_recipe("agents.agent")
def test_prune_check_history(self):
from .tasks import prune_check_history
@@ -401,8 +405,8 @@ class TestCheckTasks(TacticalTestCase):
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
for check_history in check_history_data: # type: ignore
check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore
check_history.save()
# prune data 30 days old
@@ -412,3 +416,758 @@ class TestCheckTasks(TacticalTestCase):
# prune all Check history Data
prune_check_history(0)
self.assertEqual(CheckHistory.objects.count(), 0)
def test_handle_script_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
script = baker.make_recipe("checks.script_check", agent=self.agent)
# test failing
data = {
"id": script.id,
"retcode": 500,
"stderr": "error",
"stdout": "message",
"runtime": 5.000,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=script.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test passing
data = {
"id": script.id,
"retcode": 0,
"stderr": "error",
"stdout": "message",
"runtime": 5.000,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=script.id)
self.assertEqual(new_check.status, "passing")
# test failing info
script.info_return_codes = [20, 30, 50]
script.save()
data = {
"id": script.id,
"retcode": 30,
"stderr": "error",
"stdout": "message",
"runtime": 5.000,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=script.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "info")
# test failing warning
script.warning_return_codes = [80, 100, 1040]
script.save()
data = {
"id": script.id,
"retcode": 1040,
"stderr": "error",
"stdout": "message",
"runtime": 5.000,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=script.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
def test_handle_diskspace_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
diskspace = baker.make_recipe(
"checks.diskspace_check",
warning_threshold=20,
error_threshold=10,
agent=self.agent,
)
# test warning threshold failure
data = {
"id": diskspace.id,
"exists": True,
"percent_used": 85,
"total": 500,
"free": 400,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
# test error failure
data = {
"id": diskspace.id,
"exists": True,
"percent_used": 95,
"total": 500,
"free": 400,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test disk not exist
data = {"id": diskspace.id, "exists": False}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test warning threshold 0
diskspace.warning_threshold = 0
diskspace.save()
data = {
"id": diskspace.id,
"exists": True,
"percent_used": 95,
"total": 500,
"free": 400,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test error threshold 0
diskspace.warning_threshold = 50
diskspace.error_threshold = 0
diskspace.save()
data = {
"id": diskspace.id,
"exists": True,
"percent_used": 95,
"total": 500,
"free": 400,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
# test passing
data = {
"id": diskspace.id,
"exists": True,
"percent_used": 50,
"total": 500,
"free": 400,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=diskspace.id)
self.assertEqual(new_check.status, "passing")
def test_handle_cpuload_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
cpuload = baker.make_recipe(
"checks.cpuload_check",
warning_threshold=70,
error_threshold=90,
agent=self.agent,
)
# test failing warning
data = {"id": cpuload.id, "percent": 80}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=cpuload.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
# test failing error
data = {"id": cpuload.id, "percent": 95}
# reset check history
cpuload.history = []
cpuload.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=cpuload.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test passing
data = {"id": cpuload.id, "percent": 50}
# reset check history
cpuload.history = []
cpuload.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=cpuload.id)
self.assertEqual(new_check.status, "passing")
# test warning threshold 0
cpuload.warning_threshold = 0
cpuload.save()
data = {"id": cpuload.id, "percent": 95}
# reset check history
cpuload.history = []
cpuload.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=cpuload.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test error threshold 0
cpuload.warning_threshold = 50
cpuload.error_threshold = 0
cpuload.save()
data = {"id": cpuload.id, "percent": 95}
# reset check history
cpuload.history = []
cpuload.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=cpuload.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
def test_handle_memory_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
memory = baker.make_recipe(
"checks.memory_check",
warning_threshold=70,
error_threshold=90,
agent=self.agent,
)
# test failing warning
data = {"id": memory.id, "percent": 80}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=memory.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
# test failing error
data = {"id": memory.id, "percent": 95}
# reset check history
memory.history = []
memory.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=memory.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test passing
data = {"id": memory.id, "percent": 50}
# reset check history
memory.history = []
memory.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=memory.id)
self.assertEqual(new_check.status, "passing")
# test warning threshold 0
memory.warning_threshold = 0
memory.save()
data = {"id": memory.id, "percent": 95}
# reset check history
memory.history = []
memory.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=memory.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test error threshold 0
memory.warning_threshold = 50
memory.error_threshold = 0
memory.save()
data = {"id": memory.id, "percent": 95}
# reset check history
memory.history = []
memory.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=memory.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
def test_handle_ping_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
ping = baker.make_recipe(
"checks.ping_check", agent=self.agent, alert_severity="info"
)
# test failing info
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=ping.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "info")
# test failing warning
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
ping.alert_severity = "warning"
ping.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=ping.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
# test failing error
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
ping.alert_severity = "error"
ping.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=ping.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test failing error
data = {
"id": ping.id,
"output": "some output",
"has_stdout": False,
"has_stderr": True,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=ping.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
# test passing
data = {
"id": ping.id,
"output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
"has_stdout": True,
"has_stderr": False,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=ping.id)
self.assertEqual(new_check.status, "passing")
@patch("agents.models.Agent.nats_cmd")
def test_handle_winsvc_check(self, nats_cmd):
from checks.models import Check
url = "/api/v3/checkrunner/"
winsvc = baker.make_recipe(
"checks.winsvc_check", agent=self.agent, alert_severity="info"
)
# test passing running
data = {"id": winsvc.id, "exists": True, "status": "running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
# test passing start pending
winsvc.pass_if_start_pending = True
winsvc.save()
data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
# test failing no start
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "info")
# test failing and attempt start
winsvc.restart_if_stopped = True
winsvc.alert_severity = "warning"
winsvc.save()
nats_cmd.return_value = "timeout"
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test failing and attempt start
winsvc.alert_severity = "error"
winsvc.save()
nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test success and attempt start
nats_cmd.return_value = {"success": True}
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test failing and service not exist
data = {"id": winsvc.id, "exists": False, "status": ""}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
# test success and service not exist
winsvc.pass_if_svc_not_exist = True
winsvc.save()
data = {"id": winsvc.id, "exists": False, "status": ""}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
def test_handle_eventlog_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
eventlog = baker.make_recipe(
"checks.eventlog_check",
event_type="warning",
fail_when="contains",
event_id=123,
alert_severity="warning",
agent=self.agent,
)
data = {
"id": eventlog.id,
"log": [
{
"eventType": "warning",
"eventID": 150,
"source": "source",
"message": "a test message",
},
{
"eventType": "warning",
"eventID": 123,
"source": "source",
"message": "a test message",
},
{
"eventType": "error",
"eventID": 123,
"source": "source",
"message": "a test message",
},
{
"eventType": "error",
"eventID": 123,
"source": "source",
"message": "a test message",
},
],
}
# test failing when contains
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.alert_severity, "warning")
self.assertEquals(new_check.status, "failing")
# test passing when not contains and message
eventlog.event_message = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing when not contains and message and source
eventlog.fail_when = "not_contains"
eventlog.alert_severity = "error"
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "error")
# test passing when contains with source and message
eventlog.event_message = "test"
eventlog.event_source = "source"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing with wildcard not contains and source
eventlog.event_id_is_wildcard = True
eventlog.event_source = "doesn't exist"
eventlog.event_message = ""
eventlog.event_id = 0
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "error")
# test passing with wildcard contains
eventlog.event_source = ""
eventlog.event_message = ""
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing with wildcard contains and message
eventlog.fail_when = "contains"
eventlog.event_type = "error"
eventlog.alert_severity = "info"
eventlog.event_message = "test"
eventlog.event_source = ""
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "info")
# test passing with wildcard not contains message and source
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test multiple events found and contains
# this should pass since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# test not contains
# this should fail since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "not_contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -1,31 +1,26 @@
import asyncio
from packaging import version as pyver
from django.shortcuts import get_object_or_404
from django.db.models import Q
from django.utils import timezone as djangotime
from datetime import datetime as dt
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from packaging import version as pyver
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from agents.models import Agent
from automation.models import Policy
from .models import Check
from scripts.models import Script
from .serializers import CheckSerializer, CheckHistorySerializer
from automation.tasks import (
generate_agent_checks_from_policies_task,
delete_policy_check_task,
generate_agent_checks_from_policies_task,
update_policy_check_fields_task,
)
from scripts.models import Script
from tacticalrmm.utils import notify_error
from .models import Check
from .serializers import CheckHistorySerializer, CheckSerializer
class AddCheck(APIView):
@@ -64,7 +59,7 @@ class AddCheck(APIView):
if policy:
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
elif agent:
checks = agent.agentchecks.filter(
checks = agent.agentchecks.filter( # type: ignore
check_type=obj.check_type, managed_by_policy=True
)
@@ -154,7 +149,7 @@ class CheckHistory(APIView):
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = check.check_history.filter(timeFilter).order_by("-x")
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
return Response(
CheckHistorySerializer(

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Client, Site, Deployment
from .models import Client, Deployment, Site
admin.site.register(Client)
admin.site.register(Site)

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.0.6 on 2020-05-31 01:23
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.0.7 on 2020-06-09 16:07
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1 on 2020-08-21 21:15
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,9 +1,10 @@
# Generated by Django 3.1.2 on 2020-10-25 01:03
from django.db import migrations, models
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -32,6 +32,7 @@ class Client(BaseAuditModel):
)
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
@@ -54,6 +55,9 @@ class Client(BaseAuditModel):
create_tasks=True,
)
if old_client and old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -127,6 +131,7 @@ class Site(BaseAuditModel):
)
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
@@ -149,6 +154,9 @@ class Site(BaseAuditModel):
create_tasks=True,
)
if old_site and old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)

View File

@@ -1,5 +1,6 @@
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
from .models import Client, Site, Deployment
from .models import Client, Deployment, Site
class SiteSerializer(ModelSerializer):

View File

@@ -1,14 +1,16 @@
import uuid
from tacticalrmm.test import TacticalTestCase
from model_bakery import baker
from .models import Client, Site, Deployment
from rest_framework.serializers import ValidationError
from tacticalrmm.test import TacticalTestCase
from .models import Client, Deployment, Site
from .serializers import (
ClientSerializer,
SiteSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteSerializer,
)

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -1,34 +1,26 @@
import pytz
import re
import os
import uuid
import subprocess
import datetime as dt
import re
import uuid
from django.utils import timezone as djangotime
from django.db import DataError
from django.shortcuts import get_object_or_404
import pytz
from django.conf import settings
from django.http import HttpResponse
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import api_view
from .serializers import (
ClientSerializer,
SiteSerializer,
ClientTreeSerializer,
DeploymentSerializer,
)
from .models import Client, Site, Deployment
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.utils import notify_error
from tacticalrmm.utils import generate_installer_exe, notify_error
from .models import Client, Deployment, Site
from .serializers import (
ClientSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteSerializer,
)
class GetAddClients(APIView):
@@ -188,99 +180,28 @@ class GenerateAgent(APIView):
d = get_object_or_404(Deployment, uid=uid)
go_bin = "/usr/local/rmmgo/go/bin/go"
if not os.path.exists(go_bin):
return notify_error("Missing golang")
api = f"https://{request.get_host()}"
inno = (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
if d.arch == "64"
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
)
download_url = settings.DL_64 if d.arch == "64" else settings.DL_32
client = d.client.name.replace(" ", "").lower()
site = d.site.name.replace(" ", "").lower()
client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
ext = ".exe" if d.arch == "64" else "-x86.exe"
file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
exe = os.path.join(settings.EXE_DIR, file_name)
if os.path.exists(exe):
try:
os.remove(exe)
except:
pass
goarch = "amd64" if d.arch == "64" else "386"
cmd = [
"env",
"GOOS=windows",
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={d.client.pk}'",
f"-X 'main.Site={d.site.pk}'",
f"-X 'main.Atype={d.mon_type}'",
f"-X 'main.Rdp={d.install_flags['rdp']}'",
f"-X 'main.Ping={d.install_flags['ping']}'",
f"-X 'main.Power={d.install_flags['power']}'",
f"-X 'main.DownloadUrl={download_url}'",
f"-X 'main.Token={d.token_key}'\"",
"-o",
exe,
]
gen = [
"env",
"GOOS=windows",
f"GOARCH={goarch}",
go_bin,
"generate",
]
try:
r1 = subprocess.run(
" ".join(gen),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except:
return notify_error("genfailed")
if r1.returncode != 0:
return notify_error("genfailed")
try:
r = subprocess.run(
" ".join(cmd),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except:
return notify_error("buildfailed")
if r.returncode != 0:
return notify_error("buildfailed")
if settings.DEBUG:
with open(exe, "rb") as f:
response = HttpResponse(
f.read(),
content_type="application/vnd.microsoft.portable-executable",
)
response["Content-Disposition"] = f"inline; filename={file_name}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={file_name}"
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
return response
return generate_installer_exe(
file_name=f"rmm-{client}-{site}-{d.mon_type}{ext}",
goarch="amd64" if d.arch == "64" else "386",
inno=inno,
api=f"https://{request.get_host()}",
client_id=d.client.pk,
site_id=d.site.pk,
atype=d.mon_type,
rdp=d.install_flags["rdp"],
ping=d.install_flags["ping"],
power=d.install_flags["power"],
download_url=settings.DL_64 if d.arch == "64" else settings.DL_32,
token=d.token_key,
)

View File

@@ -1,4 +1,5 @@
from django.contrib import admin
from .models import CoreSettings
admin.site.register(CoreSettings)

View File

@@ -0,0 +1,5 @@
module github.com/wh1te909/goinstaller
go 1.16
require github.com/josephspurrier/goversioninfo v1.2.0 // indirect

View File

@@ -0,0 +1,10 @@
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
@@ -27,6 +28,18 @@ var (
DownloadUrl string
)
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 900,
Transport: netTransport,
}
func downloadAgent(filepath string) (err error) {
out, err := os.Create(filepath)
@@ -35,7 +48,7 @@ func downloadAgent(filepath string) (err error) {
}
defer out.Close()
resp, err := http.Get(DownloadUrl)
resp, err := netClient.Get(DownloadUrl)
if err != nil {
return err
}
@@ -59,7 +72,6 @@ func main() {
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
flag.Parse()
var debug bool = false
@@ -93,10 +105,6 @@ func main() {
cmdArgs = append(cmdArgs, "-cert", *cert)
}
if len(strings.TrimSpace(*timeout)) != 0 {
cmdArgs = append(cmdArgs, "-timeout", *timeout)
}
if Rdp == "1" {
cmdArgs = append(cmdArgs, "-rdp")
}

View File

@@ -33,6 +33,20 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
Try
{
$DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled
if ($DefenderStatus -match "True") {
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
}
}
Catch {
# pass
}
Try
{
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...')
@@ -51,4 +65,4 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
{
Remove-Item -Path $OutPath\$output
}
}
}

View File

@@ -1,11 +1,14 @@
from django.core.management.base import BaseCommand
from django.conf import settings
from core.models import CoreSettings
from .helpers import get_auth_token
import asyncio
import websockets
import json
import websockets
from django.conf import settings
from django.core.management.base import BaseCommand
from core.models import CoreSettings
from .helpers import get_auth_token
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"

View File

@@ -1,5 +1,6 @@
import time
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

View File

@@ -1,5 +1,6 @@
from django.core.management.base import BaseCommand
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
from core.models import CoreSettings

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