Compare commits

..

140 Commits

Author SHA1 Message Date
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
wh1te909
2e5868778a Release 0.4.11 2021-02-17 23:35:00 +00:00
wh1te909
a10b8dab9b bump versions 2021-02-17 23:31:49 +00:00
wh1te909
92f4f7ef59 go 1.16 2021-02-17 23:26:56 +00:00
wh1te909
31257bd5cb Release 0.4.10 2021-02-17 19:35:51 +00:00
wh1te909
bb6510862f bump version 2021-02-17 19:35:24 +00:00
sadnub
797ecf0780 implement exclude workstations and servers. Fix excluding individual clients, sites, and agents 2021-02-17 14:19:07 -05:00
sadnub
f9536dc67f allow viewing alert script results on resolved alerts 2021-02-17 13:29:51 -05:00
sadnub
e8b95362af fix automation manager UI. Modify agent/check/task table alert checkboxes to show if it is managed by an alert template 2021-02-17 13:29:51 -05:00
sadnub
bdc39ad4ec Create alerting.md 2021-02-16 23:11:34 -05:00
wh1te909
4a202c5585 Release 0.4.9 2021-02-16 23:39:22 +00:00
wh1te909
3c6b321f73 bump version 2021-02-16 23:38:38 +00:00
wh1te909
cb29b52799 remove unused import 2021-02-16 23:14:03 +00:00
wh1te909
7e48015a54 Release 0.4.8 2021-02-16 18:57:37 +00:00
wh1te909
9ed3abf932 fix tests 2021-02-16 18:55:55 +00:00
wh1te909
61762828a3 fix typo 2021-02-16 18:50:42 +00:00
wh1te909
59beabe5ac bump versions 2021-02-16 18:47:51 +00:00
wh1te909
0b30faa28c decrease pause timeout for installer 2021-02-16 18:45:59 +00:00
wh1te909
d12d49b93f update quasar [skip ci] 2021-02-16 17:24:48 +00:00
wh1te909
f1d64d275a update go [skip ci] 2021-02-16 17:15:13 +00:00
wh1te909
d094eeeb03 update natsapi [skip ci] 2021-02-16 17:09:05 +00:00
wh1te909
be25af658e partially implement #222 2021-02-16 09:22:28 +00:00
wh1te909
794f52c229 delete remove salt task 2021-02-16 08:46:42 +00:00
wh1te909
5d4dc4ed4c change monitoragents func to run async 2021-02-16 08:33:54 +00:00
wh1te909
e49d97b898 disable loading spinner during alert poll 2021-02-16 01:06:42 +00:00
wh1te909
b6b4f1ba62 fix query 2021-02-16 01:06:12 +00:00
wh1te909
653d476716 back to http requests wh1te909/rmmagent@278b3a8a55 2021-02-14 03:12:22 +00:00
sadnub
48b855258c improve test coverage for automation 2021-02-13 16:14:15 -05:00
wh1te909
c7efdaf5f9 change run_script to take the script PK instead of entire script model 2021-02-13 19:41:38 +00:00
wh1te909
22523ed3d3 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-02-13 19:40:41 +00:00
wh1te909
33c602dd61 update reqs 2021-02-13 19:40:18 +00:00
sadnub
e2a5509b76 add missing task tests in automation and alerts 2021-02-13 14:38:03 -05:00
wh1te909
61a0fa1a89 fix runscript email 2021-02-12 22:50:21 +00:00
wh1te909
a35bd8292b catch service error 2021-02-12 19:24:06 +00:00
Tragic Bronson
06c8ae60e3 Merge pull request #269 from sadnub/feature-alerts
WIP - Feature alerts
2021-02-12 10:48:27 -08:00
sadnub
deeab1f845 fix/add tests for check thresholds 2021-02-12 13:39:46 -05:00
sadnub
da81c4c987 fix failure and resolved action timeouts 2021-02-12 12:49:16 -05:00
sadnub
d180f1b2d5 fix check threshold modals and add client/serverside validation. Allow viewing alert script results in alerts overview. Fix diskspace check history computation. other fixes and improvements 2021-02-12 12:37:53 -05:00
sadnub
526135629c fix some typos and implement runscript and runscriptfull on agent function 2021-02-11 20:11:03 -05:00
sadnub
6b9493e057 reworked alerts a bit to not need AgentOutage table. Implemented resolve/failure script running on alert. also added script arg support for alert actions. Allow scripts to be run on any running agent 2021-02-11 20:11:03 -05:00
sadnub
9bb33d2afc fix tests 2021-02-11 20:11:03 -05:00
sadnub
7421138533 finish alerts views testing. Minor bug fixes 2021-02-11 20:11:03 -05:00
sadnub
d0800c52bb black 2021-02-11 20:11:03 -05:00
sadnub
913fcd4df2 fix tests and added soem minor fixes 2021-02-11 20:11:03 -05:00
sadnub
83322cc725 fix automation tests. minor fixes 2021-02-11 20:11:03 -05:00
sadnub
5944501feb fix migrations for real this time 2021-02-11 20:11:03 -05:00
sadnub
17e3603d3d implement overriding email/sms settings with alert templates 2021-02-11 20:11:03 -05:00
sadnub
95be43ae47 fix alerts icon and fix policycheck/task status. added resolved alerts actions 2021-02-11 20:11:03 -05:00
sadnub
feb91cbbaa fix migration issue and consolidate migrations a bit 2021-02-11 20:11:03 -05:00
sadnub
79409af168 implement alert periodic notifications for agent, task, and check. implement sms/email functionality for autotasks 2021-02-11 20:11:03 -05:00
sadnub
5dbfb64822 add handle alerts functions to agents, checks, and tasks. Minor fixes 2021-02-11 20:11:03 -05:00
sadnub
5e7ebf5e69 added relation view and a number of bug fixes 2021-02-11 20:11:03 -05:00
sadnub
e73215ca74 implement alert template exclusions 2021-02-11 20:11:03 -05:00
sadnub
a5f123b9ce bug fixes with automated manager deleting policies and adding 2021-02-11 20:11:03 -05:00
sadnub
ac058e9675 fixed alerts manager table, added celery task to unsnooze alerts, added bulk actions to alerts overview 2021-02-11 20:11:02 -05:00
sadnub
371b764d1d added new alert option for dashboard alerts, added actions to be run if alert triggered on agent, random fixes 2021-02-11 20:11:02 -05:00
sadnub
66d7172e09 reworked policy add for client, site, and agent. removed vue unit tests, added alertign to auto tasks, added edit autotask capabilities for certain fields, moved policy generation logic to save method on Client, Site, Agent, Policy models 2021-02-11 20:11:02 -05:00
sadnub
99d3a8a749 more alerts work 2021-02-11 20:11:02 -05:00
sadnub
db5ff372a4 alerts overview work 2021-02-11 20:11:02 -05:00
sadnub
3fe83f81be migrations fix and finishing up automation manager rework 2021-02-11 20:11:02 -05:00
sadnub
669e638fd6 automation manager rework start 2021-02-11 20:11:02 -05:00
sadnub
f1f999f3b6 more alerts work 2021-02-11 20:11:02 -05:00
sadnub
6f3b6fa9ce alerts wip 2021-02-11 20:11:02 -05:00
wh1te909
938f945301 drop min ram req 2021-02-12 00:23:22 +00:00
Tragic Bronson
e3efb2aad6 Merge pull request #273 from wh1te909/dependabot/pip/dot-devcontainer/cryptography-3.3.2
Bump cryptography from 3.2.1 to 3.3.2 in /.devcontainer
2021-02-11 13:47:26 -08:00
Tragic Bronson
1e678c0d78 Merge pull request #272 from wh1te909/dependabot/pip/api/tacticalrmm/cryptography-3.3.2
Bump cryptography from 3.3.1 to 3.3.2 in /api/tacticalrmm
2021-02-11 13:47:14 -08:00
wh1te909
a59c111140 add community script 2021-02-11 17:58:59 +00:00
Tragic Bronson
a8b2a31bed Merge pull request #275 from bradhawkins85/patch-3
Create Display Message To User.ps1
2021-02-11 09:45:55 -08:00
bradhawkins85
37402f9ee8 Create Display Message To User.ps1 2021-02-11 11:16:28 +10:00
dependabot[bot]
e7b5ecb40f Bump cryptography from 3.2.1 to 3.3.2 in /.devcontainer
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.2.1 to 3.3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.2.1...3.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-10 02:34:29 +00:00
dependabot[bot]
c817ef04b9 Bump cryptography from 3.3.1 to 3.3.2 in /api/tacticalrmm
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.3.1...3.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-10 01:51:52 +00:00
wh1te909
f52b18439c update ssh script 2021-02-09 19:08:24 +00:00
wh1te909
1e03c628d5 Release 0.4.7 2021-02-06 01:04:02 +00:00
wh1te909
71fb39db1f bump versions 2021-02-06 00:59:49 +00:00
wh1te909
bcfb3726b0 update restore script to work on debian 10 2021-02-06 00:40:25 +00:00
wh1te909
c6e9e29671 increase uwsgi buffer size 2021-02-06 00:39:22 +00:00
wh1te909
1bfefcce39 fix backup 2021-02-06 00:38:29 +00:00
wh1te909
22488e93e1 approve updates when triggered manually 2021-02-03 23:23:34 +00:00
wh1te909
244b89f035 exclude migrations from black 2021-02-03 20:11:49 +00:00
wh1te909
1f9a241b94 Release 0.4.6 2021-02-02 19:33:30 +00:00
wh1te909
03641aae42 bump versions 2021-02-02 19:20:24 +00:00
wh1te909
a2bdd113cc update natsapi [skip ci] 2021-02-02 19:19:29 +00:00
wh1te909
a92e2f3c7b more winupdate fixes 2021-02-02 09:42:12 +00:00
wh1te909
97766b3a57 more superseded updates cleanup 2021-02-02 01:12:20 +00:00
wh1te909
9ef4c3bb06 more pending actions fix 2021-02-01 21:23:37 +00:00
wh1te909
d82f0cd757 Release 0.4.5 2021-02-01 20:57:53 +00:00
wh1te909
5f529e2af4 bump versions 2021-02-01 20:57:35 +00:00
wh1te909
beadd9e02b fix duplicate pending actions being created 2021-02-01 20:56:05 +00:00
wh1te909
72543789cb Release 0.4.4 2021-02-01 19:24:51 +00:00
wh1te909
5789439fa9 bump versions 2021-02-01 19:23:03 +00:00
wh1te909
f549126bcf update natsapi 2021-02-01 19:20:32 +00:00
wh1te909
7197548bad new pipelines vm 2021-01-31 02:42:10 +00:00
wh1te909
241fde783c add back pending actions for agent updates 2021-01-31 02:06:55 +00:00
wh1te909
2b872cd1f4 remove old views 2021-01-31 00:19:10 +00:00
wh1te909
a606fb4d1d add some deps to install for stripped down vps [skip ci] 2021-01-30 21:31:01 +00:00
wh1te909
9f9c6be38e update natsapi [skip ci] github.com/wh1te909/rmmagent@47b25c29362f0639ec606571f679df1f523e69a9 2021-01-30 06:42:20 +00:00
wh1te909
01ee524049 Release 0.4.3 2021-01-30 04:45:10 +00:00
wh1te909
af9cb65338 bump version 2021-01-30 04:44:41 +00:00
wh1te909
8aa11c580b move agents monitor task to go 2021-01-30 04:39:15 +00:00
wh1te909
ada627f444 forgot to enable natsapi during install 2021-01-30 04:28:27 +00:00
wh1te909
a7b6d338c3 update reqs 2021-01-30 02:06:56 +00:00
wh1te909
9f00538b97 fix tests 2021-01-29 23:38:59 +00:00
wh1te909
a085015282 increase timeout for security eventlogs 2021-01-29 23:34:16 +00:00
wh1te909
0b9c220fbb remove old task 2021-01-29 20:36:28 +00:00
wh1te909
0e3d04873d move wmi celery task to golang 2021-01-29 20:10:52 +00:00
wh1te909
b7578d939f add test for community script shell type 2021-01-29 09:37:34 +00:00
wh1te909
b5c28de03f Release 0.4.2 2021-01-29 08:23:06 +00:00
wh1te909
e17d25c156 bump versions 2021-01-29 08:12:03 +00:00
wh1te909
c25dc1b99c also override shell during load community scripts 2021-01-29 07:39:08 +00:00
Tragic Bronson
a493a574bd Merge pull request #265 from saulens22/patch-1
Fix "TRMM Defender Exclusions" script shell type
2021-01-28 23:36:03 -08:00
Saulius Kazokas
4284493dce Fix "TRMM Defender Exclusions" script shell type 2021-01-29 07:10:10 +02:00
wh1te909
25059de8e1 fix superseded windows defender updates 2021-01-29 02:37:51 +00:00
wh1te909
1731b05ad0 remove old serializers 2021-01-29 02:25:31 +00:00
wh1te909
e80dc663ac remove unused func 2021-01-29 02:22:06 +00:00
wh1te909
39988a4c2f cleanup an old view 2021-01-29 02:15:27 +00:00
wh1te909
415bff303a add some debug for unsupported agents 2021-01-29 01:22:35 +00:00
wh1te909
a65eb62a54 checkrunner changes wh1te909/rmmagent@10a0935f1b 2021-01-29 00:34:18 +00:00
wh1te909
03b2982128 update build flags 2021-01-28 23:11:32 +00:00
239 changed files with 11170 additions and 7475 deletions

View File

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

View File

@@ -15,7 +15,7 @@ RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical
# Copy Go Files
COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
# Copy Dev python reqs
COPY ./requirements.txt /

View File

@@ -3,6 +3,7 @@ version: '3.4'
services:
api-dev:
image: api-dev
restart: always
build:
context: .
dockerfile: ./api.dockerfile
@@ -21,6 +22,7 @@ services:
app-dev:
image: node:12-alpine
restart: always
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
working_dir: /workspace/web
volumes:

View File

@@ -45,7 +45,7 @@ function django_setup {
echo "setting up django environment"
# configure django settings
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)"
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
@@ -106,29 +106,28 @@ EOF
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
# run migrations and init scripts
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py initial_db_setup
python manage.py initial_mesh_setup
python manage.py load_chocos
python manage.py load_community_scripts
python manage.py reload_nats
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
"${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
"${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
"${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
# create super user
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
}
if [ "$1" = 'tactical-init-dev' ]; then
# make directories if they don't exist
mkdir -p ${TACTICAL_DIR}/tmp
mkdir -p "${TACTICAL_DIR}/tmp"
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# setup Python virtual env and install dependencies
test -f ${VIRTUAL_ENV} && python -m venv --copies ${VIRTUAL_ENV}
pip install --no-cache-dir -r /requirements.txt
! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
django_setup
@@ -150,20 +149,20 @@ EOF
fi
if [ "$1" = 'tactical-api' ]; then
cp ${WORKSPACE_DIR}/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
chmod +x /usr/local/bin/goversioninfo
check_tactical_ready
python manage.py runserver 0.0.0.0:${API_PORT}
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
fi
if [ "$1" = 'tactical-celery-dev' ]; then
check_tactical_ready
env/bin/celery -A tacticalrmm worker -l debug
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug
fi
if [ "$1" = 'tactical-celerybeat-dev' ]; then
check_tactical_ready
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
env/bin/celery -A tacticalrmm beat -l debug
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
fi

View File

@@ -1,39 +1,38 @@
# 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==2.6.1
amqp==5.0.5
asgiref==3.3.1
asyncio-nats-client==0.11.4
billiard==3.6.3.0
celery==4.4.6
celery==5.0.5
certifi==2020.12.5
cffi==1.14.3
chardet==3.0.4
cryptography==3.2.1
cffi==1.14.5
chardet==4.0.0
cryptography==3.4.4
decorator==4.4.2
Django==3.1.4
django-cors-headers==3.5.0
Django==3.1.6
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.2
future==0.18.2
idna==2.10
kombu==4.6.11
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.0
packaging==20.4
msgpack==1.0.2
packaging==20.8
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.9.9
pyotp==2.4.1
pycryptodome==3.10.1
pyotp==2.6.0
pyparsing==2.4.7
pytz==2020.4
pytz==2021.1
qrcode==6.1
redis==3.5.3
requests==2.25.0
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.49.0
urllib3==1.26.2
validators==0.18.1
vine==1.3.0
twilio==6.52.0
urllib3==1.26.3
validators==0.18.2
vine==5.0.0
websockets==8.1
zipp==3.4.0
black
@@ -42,3 +41,6 @@ django-extensions
coverage
coveralls
model_bakery
mkdocs
mkdocs-material
pymdown-extensions

View File

@@ -36,7 +36,7 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
## Installation
### Requirements
- VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
- 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)

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,6 +1,8 @@
import pyotp
import subprocess
import pyotp
from django.core.management.base import BaseCommand
from accounts.models import User

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

@@ -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

View File

@@ -1,9 +1,5 @@
import pyotp
from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
)
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import 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):
@@ -278,15 +279,11 @@ class TestUserAction(TacticalTestCase):
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"agent_dblclick_action": "editagent"}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"agent_dblclick_action": "remotebg"}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"agent_dblclick_action": "takecontrol"}
data = {
"userui": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

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

View File

@@ -1,23 +1,20 @@
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
class CheckCreds(KnoxLoginView):

View File

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

View File

@@ -1,14 +1,12 @@
import json
import os
import random
import string
import os
import json
from model_bakery.recipe import Recipe, seq
from itertools import cycle
from django.utils import timezone as djangotime
from django.conf import settings
from .models import Agent
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery.recipe import Recipe, foreign_key
def generate_agent_id(hostname):
@@ -16,6 +14,9 @@ def generate_agent_id(hostname):
return f"{rand}-{hostname}"
site = Recipe("clients.Site")
def get_wmi_data():
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
@@ -24,7 +25,8 @@ def get_wmi_data():
agent = Recipe(
Agent,
"agents.Agent",
site=foreign_key(site),
hostname="DESKTOP-TEST123",
version="1.3.0",
monitoring_type=cycle(["workstation", "server"]),

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,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0026_auto_20201125_2334'),
]
operations = [
migrations.AddField(
model_name='agent',
name='overdue_dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-06 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0027_agent_overdue_dashboard_alert'),
]
operations = [
migrations.AddField(
model_name='agentoutage',
name='outage_email_sent_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='agentoutage',
name='outage_sms_sent_time',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.4 on 2021-02-10 21:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0028_auto_20210206_1534'),
]
operations = [
migrations.DeleteModel(
name='AgentOutage',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-02-16 08:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0029_delete_agentoutage'),
]
operations = [
migrations.AddField(
model_name='agent',
name='offline_time',
field=models.PositiveIntegerField(default=4),
),
]

View File

@@ -1,25 +1,27 @@
import time
import asyncio
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
import time
from collections import Counter
from typing import List
from loguru import logger
from packaging import version as pyver
from distutils.version import LooseVersion
from typing import Any, List, 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 core.models import CoreSettings, TZ_CHOICES
from alerts.models import AlertTemplate
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
logger.configure(**settings.LOG_CONFIG)
@@ -50,6 +52,8 @@ class Agent(BaseAuditModel):
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
overdue_email_alert = models.BooleanField(default=False)
overdue_text_alert = models.BooleanField(default=False)
overdue_dashboard_alert = models.BooleanField(default=False)
offline_time = models.PositiveIntegerField(default=4)
overdue_time = models.PositiveIntegerField(default=30)
check_interval = models.PositiveIntegerField(default=120)
needs_reboot = models.BooleanField(default=False)
@@ -75,6 +79,24 @@ class Agent(BaseAuditModel):
on_delete=models.SET_NULL,
)
def save(self, *args, **kwargs):
# get old agent if exists
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
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
if (
not old_agent
or old_agent
and old_agent.policy != self.policy
or old_agent.site != self.site
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
def __str__(self):
return self.hostname
@@ -127,7 +149,7 @@ class Agent(BaseAuditModel):
@property
def status(self):
offline = djangotime.now() - djangotime.timedelta(minutes=6)
offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
if self.last_seen is not None:
@@ -248,6 +270,63 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
def run_script(
self,
scriptpk: int,
args: List[str] = [],
timeout: int = 120,
full: bool = False,
wait: bool = False,
run_on_any: bool = False,
) -> Any:
from scripts.models import Script
script = Script.objects.get(pk=scriptpk)
data = {
"func": "runscriptfull" if full else "runscript",
"timeout": timeout,
"script_args": args,
"payload": {
"code": script.code,
"shell": script.shell,
},
}
running_agent = self
if run_on_any:
nats_ping = {"func": "ping", "timeout": 1}
# try on self first
r = asyncio.run(self.nats_cmd(nats_ping))
if r == "pong":
running_agent = self
else:
online = [
agent
for agent in Agent.objects.only(
"pk", "last_seen", "overdue_time", "offline_time"
)
if agent.status == "online"
]
for agent in online:
r = asyncio.run(agent.nats_cmd(nats_ping))
if r == "pong":
running_agent = agent
break
if running_agent.pk == self.pk:
return "Unable to find an online agent"
if wait:
return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
else:
asyncio.run(running_agent.nats_cmd(data, wait=False))
return "ok"
# auto approves updates
def approve_updates(self):
patch_policy = self.get_patch_policy()
@@ -381,6 +460,113 @@ class Agent(BaseAuditModel):
)
)
# 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]:
site = self.site
client = self.client
core = CoreSettings.objects.first()
templates = list()
# check if alert template is on a policy assigned to agent
if (
self.policy
and self.policy.alert_template
and self.policy.alert_template.is_active
):
templates.append(self.policy.alert_template)
# check if policy with alert template is assigned to the site
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)
if (
self.monitoring_type == "workstation"
and site.workstation_policy
and site.workstation_policy.alert_template
and site.workstation_policy.alert_template.is_active
):
templates.append(site.workstation_policy.alert_template)
# check if alert template is assigned to site
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
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)
if (
self.monitoring_type == "workstation"
and client.workstation_policy
and client.workstation_policy.alert_template
and client.workstation_policy.alert_template.is_active
):
templates.append(client.workstation_policy.alert_template)
# check if alert template is on client and return
if client.alert_template and client.alert_template.is_active:
templates.append(client.alert_template)
# check if alert template is applied globally and return
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
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)
if (
self.monitoring_type == "workstation"
and core.workstation_policy
and core.workstation_policy.alert_template
and core.workstation_policy.alert_template.is_active
):
templates.append(core.workstation_policy.alert_template)
# go through the templates and return the first one that isn't excluded
for template in templates:
# check if client, site, or agent has been excluded from template
if (
client.pk
in template.excluded_clients.all().values_list("pk", flat=True)
or site.pk in template.excluded_sites.all().values_list("pk", flat=True)
or self.pk
in template.excluded_agents.all()
.only("pk")
.values_list("pk", flat=True)
):
continue
# check if template is excluding 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:
return template
# no alert templates found or agent has been excluded
return None
def generate_checks_from_policies(self):
from automation.models import Policy
@@ -498,8 +684,8 @@ class Agent(BaseAuditModel):
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"]
@@ -520,73 +706,203 @@ class Agent(BaseAuditModel):
if action.details["task_id"] == task_id:
action.delete()
def handle_alert(self, checkin: bool = False) -> None:
from agents.tasks import (
agent_outage_email_task,
agent_outage_sms_task,
agent_recovery_email_task,
agent_recovery_sms_task,
)
from alerts.models import Alert
class AgentOutage(models.Model):
agent = models.ForeignKey(
Agent,
related_name="agentoutages",
null=True,
blank=True,
on_delete=models.CASCADE,
)
outage_time = models.DateTimeField(auto_now_add=True)
recovery_time = models.DateTimeField(null=True, blank=True)
outage_email_sent = models.BooleanField(default=False)
outage_sms_sent = models.BooleanField(default=False)
recovery_email_sent = models.BooleanField(default=False)
recovery_sms_sent = models.BooleanField(default=False)
# return if agent is in maintenace mode
if self.maintenance_mode:
return
@property
def is_active(self):
return False if self.recovery_time else True
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 (
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,
)
# 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 isinstance(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.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue",
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
f"Data has not been received from client {self.agent.client.name}, "
f"site {self.agent.site.name}, "
f"agent {self.agent.hostname} "
f"Data has not been received from client {self.client.name}, "
f"site {self.site.name}, "
f"agent {self.hostname} "
"within the expected time."
),
alert_template=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.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received",
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
(
f"Data has been received from client {self.agent.client.name}, "
f"site {self.agent.site.name}, "
f"agent {self.agent.hostname} "
f"Data has been received from client {self.client.name}, "
f"site {self.site.name}, "
f"agent {self.hostname} "
"after an interruption in data transmission."
),
alert_template=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.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue"
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=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.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received"
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
)
def __str__(self):
return self.agent.hostname
RECOVERY_CHOICES = [
("salt", "Salt"),

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
@@ -34,6 +32,17 @@ class AgentSerializer(serializers.ModelSerializer):
]
class AgentOverdueActionSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"pk",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
]
class AgentTableSerializer(serializers.ModelSerializer):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField()
@@ -44,6 +53,21 @@ class AgentTableSerializer(serializers.ModelSerializer):
site_name = serializers.ReadOnlyField(source="site.name")
logged_username = serializers.SerializerMethodField()
italic = serializers.SerializerMethodField()
policy = serializers.ReadOnlyField(source="policy.id")
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
alert_template = obj.get_alert_template()
if not 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,
}
def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count()
@@ -54,7 +78,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":
@@ -71,6 +95,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
model = Agent
fields = [
"id",
"alert_template",
"hostname",
"agent_id",
"site_name",
@@ -83,12 +108,14 @@ class AgentTableSerializer(serializers.ModelSerializer):
"status",
"overdue_text_alert",
"overdue_email_alert",
"overdue_dashboard_alert",
"last_seen",
"boot_time",
"checks",
"maintenance_mode",
"logged_username",
"italic",
"policy",
]
depth = 2
@@ -114,10 +141,12 @@ class AgentEditSerializer(serializers.ModelSerializer):
"timezone",
"check_interval",
"overdue_time",
"offline_time",
"overdue_text_alert",
"overdue_email_alert",
"all_timezones",
"winupdatepolicy",
"policy",
]

View File

@@ -1,65 +1,37 @@
import asyncio
from loguru import logger
from time import sleep
import datetime as dt
import random
from packaging import version as pyver
from typing import List
from time import sleep
from typing import List, Union
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, AgentOutage
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)
def _check_agent_service(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong":
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery"
)
data = {"func": "recover", "payload": {"mode": "tacagent"}}
asyncio.run(agent.nats_cmd(data, wait=False))
def _check_in_full(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False))
@app.task
def check_in_task() -> None:
q = Agent.objects.only("pk", "version")
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) == pyver.parse("1.1.12")
]
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
for chunk in chunks:
for pk in chunk:
_check_in_full(pk)
sleep(0.1)
rand = random.randint(3, 7)
sleep(rand)
@app.task
def monitor_agents_task() -> None:
q = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
for agent in agents:
_check_agent_service(agent)
def agent_update(pk: int) -> str:
agent = Agent.objects.get(pk=pk)
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
logger.warning(
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
)
return "not supported"
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
logger.warning(
f"Unable to determine arch on {agent.hostname}. Skipping agent update."
)
return "noarch"
# removed sqlite in 1.4.0 to get rid of cgo dependency
@@ -75,51 +47,38 @@ def agent_update(pk: int) -> str:
)
url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
if agent.has_nats:
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
action = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
if pyver.parse(action.details["version"]) < pyver.parse(version):
action.delete()
else:
return "pending"
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
else:
nats_data = {
"func": "agentupdate",
"payload": {
"url": url,
"version": version,
"inno": inno,
},
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
return "created"
return "not supported"
nats_data = {
"func": "agentupdate",
"payload": {
"url": url,
"version": version,
"inno": inno,
},
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created"
@app.task
def send_agent_update_task(pks: List[int], version: str) -> None:
q = Agent.objects.filter(pk__in=pks)
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
]
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
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:
agent_update(pk)
@@ -149,103 +108,94 @@ def auto_self_agent_update_task() -> None:
@app.task
def get_wmi_task():
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
]
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
for chunk in chunks:
for agent in chunk:
asyncio.run(agent.nats_cmd({"func": "wmi"}, wait=False))
sleep(0.1)
rand = random.randint(3, 7)
sleep(rand)
def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
if not alert.email_sent:
sleep(random.randint(1, 15))
alert.agent.send_outage_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
else:
if alert_interval:
# send an email only if the last email sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta:
sleep(random.randint(1, 10))
alert.agent.send_outage_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
return "ok"
@app.task
def sync_sysinfo_task():
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.1.3")
and pyver.parse(i.version) <= pyver.parse("1.1.12")
and i.status == "online"
]
def agent_recovery_email_task(pk: int) -> str:
from alerts.models import Alert
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
for chunk in chunks:
for agent in chunk:
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
sleep(0.1)
rand = random.randint(3, 7)
sleep(rand)
@app.task
def agent_outage_email_task(pk):
sleep(random.randint(1, 15))
outage = AgentOutage.objects.get(pk=pk)
outage.send_outage_email()
outage.outage_email_sent = True
outage.save(update_fields=["outage_email_sent"])
alert = Alert.objects.get(pk=pk)
alert.agent.send_recovery_email()
alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"])
return "ok"
@app.task
def agent_recovery_email_task(pk):
sleep(random.randint(1, 15))
outage = AgentOutage.objects.get(pk=pk)
outage.send_recovery_email()
outage.recovery_email_sent = True
outage.save(update_fields=["recovery_email_sent"])
def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
if not alert.sms_sent:
sleep(random.randint(1, 15))
alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
else:
if alert_interval:
# send an sms only if the last sms sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta:
sleep(random.randint(1, 10))
alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
return "ok"
@app.task
def agent_outage_sms_task(pk):
def agent_recovery_sms_task(pk: int) -> str:
from alerts.models import Alert
sleep(random.randint(1, 3))
outage = AgentOutage.objects.get(pk=pk)
outage.send_outage_sms()
outage.outage_sms_sent = True
outage.save(update_fields=["outage_sms_sent"])
alert = Alert.objects.get(pk=pk)
alert.agent.send_recovery_sms()
alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task
def agent_recovery_sms_task(pk):
sleep(random.randint(1, 3))
outage = AgentOutage.objects.get(pk=pk)
outage.send_recovery_sms()
outage.recovery_sms_sent = True
outage.save(update_fields=["recovery_sms_sent"])
@app.task
def agent_outages_task():
def agent_outages_task() -> None:
agents = Agent.objects.only(
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
"pk",
"last_seen",
"offline_time",
"overdue_time",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
)
for agent in agents:
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
outages = AgentOutage.objects.filter(agent=agent)
if outages and outages.last().is_active:
continue
outage = AgentOutage(agent=agent)
outage.save()
# add a null check history to allow gaps in graph
for check in agent.agentchecks.all():
check.add_check_history(None)
if agent.overdue_email_alert and not agent.maintenance_mode:
agent_outage_email_task.delay(pk=outage.pk)
if agent.overdue_text_alert and not agent.maintenance_mode:
agent_outage_sms_task.delay(pk=outage.pk)
if agent.status == "overdue":
agent.handle_alert()
@app.task
@@ -264,12 +214,17 @@ def handle_agent_recovery_task(pk: int) -> None:
@app.task
def run_script_email_results_task(
agentpk: int, scriptpk: int, nats_timeout: int, nats_data: dict, 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)
nats_data["func"] = "runscriptfull"
r = asyncio.run(agent.nats_cmd(nats_data, timeout=nats_timeout))
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
@@ -309,18 +264,3 @@ def run_script_email_results_task(
server.quit()
except Exception as e:
logger.error(e)
@app.task
def remove_salt_task() -> None:
if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT:
return
q = Agent.objects.only("pk", "version")
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")]
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
for chunk in chunks:
for agent in chunk:
asyncio.run(agent.nats_cmd({"func": "removesalt"}, wait=False))
sleep(0.1)
sleep(4)

View File

@@ -1,20 +1,21 @@
import json
import os
from itertools import cycle
from typing import List
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
from django.test import TestCase, override_settings
from django.conf import settings
from django.utils import timezone as djangotime
from logs.models import PendingAction
from model_bakery import baker
from packaging import version as pyver
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
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 TestAgentViews(TacticalTestCase):
@@ -64,12 +65,34 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.tasks.send_agent_update_task.delay")
def test_update_agents(self, mock_task):
url = "/agents/updateagents/"
data = {"pks": [1, 2, 3, 5, 10], "version": "0.11.1"}
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version=settings.LATEST_AGENT_VER,
_quantity=15,
)
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.3.0",
_quantity=15,
)
pks: List[int] = list(
Agent.objects.only("pk", "version").values_list("pk", flat=True)
)
data = {"pks": pks}
expected: List[int] = [
i.pk
for i in Agent.objects.only("pk", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(pks=data["pks"], version=data["version"])
mock_task.assert_called_with(pks=expected)
self.check_not_authenticated("post", url)
@@ -162,18 +185,44 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
def test_get_event_log(self, mock_ret):
url = f"/agents/{self.agent.pk}/geteventlog/Application/30/"
def test_get_event_log(self, nats_cmd):
url = f"/agents/{self.agent.pk}/geteventlog/Application/22/"
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
) as f:
mock_ret.return_value = json.load(f)
nats_cmd.return_value = json.load(f)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": "Application",
"days": str(22),
},
},
timeout=32,
)
mock_ret.return_value = "timeout"
url = f"/agents/{self.agent.pk}/geteventlog/Security/6/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 180,
"payload": {
"logname": "Security",
"days": str(6),
},
},
timeout=182,
)
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
@@ -308,7 +357,6 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"])
self.assertNotIn("power", r.json()["cmd"])
self.assertNotIn("ping", r.json()["cmd"])
data.update({"ping": 1, "power": 1})
r = self.client.post(url, data, format="json")
@@ -381,6 +429,7 @@ class TestAgentViews(TacticalTestCase):
"site": site.id,
"monitoring_type": "workstation",
"description": "asjdk234andasd",
"offline_time": 4,
"overdue_time": 300,
"check_interval": 60,
"overdue_email_alert": True,
@@ -479,42 +528,20 @@ class TestAgentViews(TacticalTestCase):
def test_overdue_action(self):
url = "/agents/overdueaction/"
payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"}
payload = {"pk": self.agent.pk, "overdue_email_alert": True}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "disabled"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "enabled"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "disabled"})
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "523423"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload.update({"alertType": "text", "action": "asdasd3434asdasd"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)
def test_list_agents_no_detail(self):
@@ -676,6 +703,7 @@ class TestAgentViews(TacticalTestCase):
class TestAgentViewsNew(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_agent_counts(self):
url = "/agents/agent_counts/"
@@ -686,15 +714,12 @@ class TestAgentViewsNew(TacticalTestCase):
monitoring_type=cycle(["server", "workstation"]),
_quantity=6,
)
agents = baker.make_recipe(
baker.make_recipe(
"agents.overdue_agent",
monitoring_type=cycle(["server", "workstation"]),
_quantity=6,
)
# make an AgentOutage for every overdue agent
baker.make("agents.AgentOutage", agent=cycle(agents), _quantity=6)
# returned data should be this
data = {
"total_server_count": 6,
@@ -758,26 +783,28 @@ class TestAgentTasks(TacticalTestCase):
agent_noarch = baker.make_recipe(
"agents.agent",
operating_system="Error getting OS",
version="1.1.11",
version=settings.LATEST_AGENT_VER,
)
r = agent_update(agent_noarch.pk)
self.assertEqual(r, "noarch")
self.assertEqual(
PendingAction.objects.filter(
agent=agent_noarch, action_type="agentupdate"
).count(),
0,
)
agent64_111 = baker.make_recipe(
agent_1111 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.11",
)
r = agent_update(agent_1111.pk)
self.assertEqual(r, "not supported")
r = agent_update(agent64_111.pk)
agent64_1112 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.12",
)
r = agent_update(agent64_1112.pk)
self.assertEqual(r, "created")
action = PendingAction.objects.get(agent__pk=agent64_111.pk)
action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(
@@ -786,6 +813,17 @@ class TestAgentTasks(TacticalTestCase):
)
self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
self.assertEqual(action.details["version"], "1.3.0")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
"version": "1.3.0",
"inno": "winagent-v1.3.0.exe",
},
},
wait=False,
)
agent_64_130 = baker.make_recipe(
"agents.agent",
@@ -806,128 +844,34 @@ class TestAgentTasks(TacticalTestCase):
},
wait=False,
)
action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
agent64_old = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.2.1",
)
nats_cmd.return_value = "ok"
r = agent_update(agent64_old.pk)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
"version": "1.3.0",
"inno": "winagent-v1.3.0.exe",
},
},
wait=False,
)
""" @patch("agents.models.Agent.salt_api_async")
@patch("agents.tasks.agent_update")
@patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
# test 64bit golang agent
self.agent64 = baker.make_recipe(
def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0",
version=settings.LATEST_AGENT_VER,
_quantity=23,
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
"url": settings.DL_64,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64.delete()
salt_api_async.reset_mock()
# test 32bit golang agent
self.agent32 = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="1.0.0",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
"url": settings.DL_32,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent32.delete()
salt_api_async.reset_mock()
# test agent that has a null os field
self.agentNone = baker.make_recipe(
"agents.agent",
operating_system=None,
version="1.0.0",
)
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
self.agentNone.delete()
salt_api_async.reset_mock()
# test auto update disabled in global settings
self.agent64 = baker.make_recipe(
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0",
version="1.3.0",
_quantity=33,
)
self.coresettings.agent_auto_update = False
self.coresettings.save(update_fields=["agent_auto_update"])
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
# reset core settings
self.agent64.delete()
salt_api_async.reset_mock()
r = auto_self_agent_update_task.s().apply()
self.assertEqual(agent_update.call_count, 0)
self.coresettings.agent_auto_update = True
self.coresettings.save(update_fields=["agent_auto_update"])
# test 64bit python agent
self.agent64py = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2.exe",
"url": OLD_64_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64py.delete()
salt_api_async.reset_mock()
# test 32bit python agent
self.agent32py = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2-x86.exe",
"url": OLD_32_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS") """
r = auto_self_agent_update_task.s().apply()
self.assertEqual(agent_update.call_count, 33)

View File

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

View File

@@ -1,46 +1,40 @@
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
import subprocess
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 generics, 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, AgentOutage, RecoveryAction, Note
from core.models import CoreSettings
from scripts.models import Script
from logs.models import AuditLog, PendingAction
from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from .models import Agent, Note, RecoveryAction
from .serializers import (
AgentSerializer,
AgentHostnameSerializer,
AgentTableSerializer,
AgentEditSerializer,
AgentHostnameSerializer,
AgentOverdueActionSerializer,
AgentSerializer,
AgentTableSerializer,
NoteSerializer,
NotesSerializer,
)
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 .tasks import run_script_email_results_task, send_agent_update_task
logger.configure(**settings.LOG_CONFIG)
@@ -58,9 +52,13 @@ def get_agent_versions(request):
@api_view(["POST"])
def update_agents(request):
pks = request.data["pks"]
version = request.data["version"]
send_agent_update_task.delay(pks=pks, version=version)
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
pks: List[int] = [
i.pk
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
send_agent_update_task.delay(pks=pks)
return Response("ok")
@@ -92,22 +90,17 @@ def uninstall(request):
def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"])
old_site = agent.site.pk
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True)
a_serializer.save()
policy = agent.winupdatepolicy.get()
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
p_serializer.is_valid(raise_exception=True)
p_serializer.save()
# check if site changed and initiate generating correct policies
if old_site != request.data["site"]:
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get()
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
p_serializer.is_valid(raise_exception=True)
p_serializer.save()
return Response("ok")
@@ -183,15 +176,16 @@ def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = 180 if logtype == "Security" else 30
data = {
"func": "eventlog",
"timeout": 30,
"timeout": timeout,
"payload": {
"logname": logtype,
"days": str(days),
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=32))
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout":
return notify_error("Unable to contact the agent")
@@ -242,6 +236,7 @@ class AgentsTableList(generics.ListAPIView):
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
@@ -254,9 +249,7 @@ class AgentsTableList(generics.ListAPIView):
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)
@@ -290,6 +283,7 @@ def by_client(request, clientpk):
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
@@ -298,7 +292,7 @@ def by_client(request, clientpk):
"maintenance_mode",
)
)
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
ctx = {"default_tz": get_default_timezone()}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@@ -319,6 +313,7 @@ def by_site(request, sitepk):
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
@@ -327,32 +322,18 @@ def by_site(request, sitepk):
"maintenance_mode",
)
)
ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
ctx = {"default_tz": get_default_timezone()}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@api_view(["POST"])
def overdue_action(request):
pk = request.data["pk"]
alert_type = request.data["alertType"]
action = request.data["action"]
agent = get_object_or_404(Agent, pk=pk)
if alert_type == "email" and action == "enabled":
agent.overdue_email_alert = True
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "email" and action == "disabled":
agent.overdue_email_alert = False
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "text" and action == "enabled":
agent.overdue_text_alert = True
agent.save(update_fields=["overdue_text_alert"])
elif alert_type == "text" and action == "disabled":
agent.overdue_text_alert = False
agent.save(update_fields=["overdue_text_alert"])
else:
return Response(
{"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
)
agent = get_object_or_404(Agent, pk=request.data["pk"])
serializer = AgentOverdueActionSerializer(
instance=agent, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(agent.hostname)
@@ -473,7 +454,7 @@ def install_agent(request):
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-X 'main.Inno={inno}'",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'",
@@ -571,12 +552,10 @@ def install_agent(request):
"/VERYSILENT",
"/SUPPRESSMSGBOXES",
"&&",
"timeout",
"/t",
"10",
"/nobreak",
">",
"NUL",
"ping",
"127.0.0.1",
"-n",
"5",
"&&",
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
"-m",
@@ -706,6 +685,7 @@ def run_script(request):
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(
@@ -714,19 +694,12 @@ def run_script(request):
script=script.name,
)
data = {
"func": "runscript",
"timeout": request.data["timeout"],
"script_args": request.data["args"],
"payload": {
"code": script.code,
"shell": script.shell,
},
}
if output == "wait":
r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout))
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")
@@ -738,13 +711,13 @@ def run_script(request):
agentpk=agent.pk,
scriptpk=script.pk,
nats_timeout=req_timeout,
nats_data=data,
emails=emails,
args=args,
)
return Response(f"{script.name} will now be run on {agent.hostname}")
else:
asyncio.run(agent.nats_cmd(data, wait=False))
return Response(f"{script.name} will now be run on {agent.hostname}")
agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view()
@@ -865,20 +838,43 @@ def bulk(request):
@api_view(["POST"])
def agent_counts(request):
server_offline_count = len(
[
agent
for agent in Agent.objects.filter(monitoring_type="server").only(
"pk",
"last_seen",
"overdue_time",
"offline_time",
)
if not agent.status == "online"
]
)
workstation_offline_count = len(
[
agent
for agent in Agent.objects.filter(monitoring_type="workstation").only(
"pk",
"last_seen",
"overdue_time",
"offline_time",
)
if not agent.status == "online"
]
)
return Response(
{
"total_server_count": Agent.objects.filter(
monitoring_type="server"
).count(),
"total_server_offline_count": AgentOutage.objects.filter(
recovery_time=None, agent__monitoring_type="server"
).count(),
"total_server_offline_count": server_offline_count,
"total_workstation_count": Agent.objects.filter(
monitoring_type="workstation"
).count(),
"total_workstation_offline_count": AgentOutage.objects.filter(
recovery_time=None, agent__monitoring_type="workstation"
).count(),
"total_workstation_offline_count": workstation_offline_count,
}
)

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Alert
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):
@@ -42,4 +42,4 @@ class Migration(migrations.Migration):
),
],
),
]
]

View File

@@ -27,4 +27,4 @@ class Migration(migrations.Migration):
max_length=100,
),
),
]
]

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):
@@ -28,4 +28,4 @@ class Migration(migrations.Migration):
name="alert_time",
field=models.DateTimeField(auto_now_add=True, null=True),
),
]
]

View File

@@ -0,0 +1,172 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0029_delete_agentoutage'),
('clients', '0008_auto_20201103_1430'),
('autotasks', '0017_auto_20210210_1512'),
('scripts', '0005_auto_20201207_1606'),
('alerts', '0003_auto_20201021_1815'),
]
operations = [
migrations.AddField(
model_name='alert',
name='action_execution_time',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alert',
name='action_retcode',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_run',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_stderr',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_stdout',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_timeout',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='alert_type',
field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
),
migrations.AddField(
model_name='alert',
name='assigned_task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'),
),
migrations.AddField(
model_name='alert',
name='email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='alert',
name='resolved_action_execution_time',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_retcode',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_run',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_stderr',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_stdout',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_timeout',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_on',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_sms_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='sms_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='snoozed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='alert',
name='severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
),
migrations.CreateModel(
name='AlertTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('is_active', models.BooleanField(default=True)),
('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
('email_from', models.EmailField(blank=True, max_length=254, null=True)),
('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_email', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_text', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('check_always_email', models.BooleanField(blank=True, default=False, null=True)),
('check_always_text', models.BooleanField(blank=True, default=False, null=True)),
('check_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('task_always_email', models.BooleanField(blank=True, default=False, null=True)),
('task_always_text', models.BooleanField(blank=True, default=False, null=True)),
('task_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')),
('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')),
('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')),
('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')),
('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')),
],
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.4 on 2021-02-12 17:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
]
operations = [
migrations.RemoveField(
model_name='alert',
name='action_timeout',
),
migrations.RemoveField(
model_name='alert',
name='resolved_action_timeout',
),
migrations.AddField(
model_name='alerttemplate',
name='action_timeout',
field=models.PositiveIntegerField(default=15),
),
migrations.AddField(
model_name='alerttemplate',
name='resolved_action_timeout',
field=models.PositiveIntegerField(default=15),
),
]

View File

@@ -0,0 +1,72 @@
# Generated by Django 3.1.6 on 2021-02-17 17:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0005_auto_20210212_1745'),
]
operations = [
migrations.RemoveField(
model_name='alerttemplate',
name='agent_include_desktops',
),
migrations.AddField(
model_name='alerttemplate',
name='exclude_servers',
field=models.BooleanField(blank=True, default=False, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='exclude_workstations',
field=models.BooleanField(blank=True, default=False, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='agent_always_alert',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='agent_always_email',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='agent_always_text',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='check_always_alert',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='check_always_email',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='check_always_text',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='task_always_alert',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='task_always_email',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='task_always_text',
field=models.BooleanField(blank=True, default=None, null=True),
),
]

View File

@@ -1,5 +1,7 @@
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
SEVERITY_CHOICES = [
("info", "Informational"),
@@ -7,6 +9,13 @@ SEVERITY_CHOICES = [
("error", "Error"),
]
ALERT_TYPE_CHOICES = [
("availability", "Availability"),
("check", "Check"),
("task", "Task"),
("custom", "Custom"),
]
class Alert(models.Model):
agent = models.ForeignKey(
@@ -23,21 +32,255 @@ class Alert(models.Model):
null=True,
blank=True,
)
assigned_task = models.ForeignKey(
"autotasks.AutomatedTask",
related_name="alert",
on_delete=models.CASCADE,
null=True,
blank=True,
)
alert_type = models.CharField(
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
)
message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
snoozed = models.BooleanField(default=False)
snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False)
severity = models.CharField(
max_length=100, choices=SEVERITY_CHOICES, default="info"
resolved_on = models.DateTimeField(null=True, blank=True)
severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
email_sent = models.DateTimeField(null=True, blank=True)
resolved_email_sent = models.DateTimeField(null=True, blank=True)
sms_sent = models.DateTimeField(null=True, blank=True)
resolved_sms_sent = models.DateTimeField(null=True, blank=True)
hidden = models.BooleanField(default=False)
action_run = models.DateTimeField(null=True, blank=True)
action_stdout = models.TextField(null=True, blank=True)
action_stderr = models.TextField(null=True, blank=True)
action_retcode = models.IntegerField(null=True, blank=True)
action_execution_time = models.CharField(max_length=100, null=True, blank=True)
resolved_action_run = models.DateTimeField(null=True, blank=True)
resolved_action_stdout = models.TextField(null=True, blank=True)
resolved_action_stderr = models.TextField(null=True, blank=True)
resolved_action_retcode = models.IntegerField(null=True, blank=True)
resolved_action_execution_time = models.CharField(
max_length=100, null=True, blank=True
)
def __str__(self):
return self.message
def resolve(self):
self.resolved = True
self.resolved_on = djangotime.now()
self.snoozed = False
self.snooze_until = None
self.save()
@classmethod
def create_availability_alert(cls, agent):
pass
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 overdue.",
hidden=True,
)
@classmethod
def create_check_alert(cls, check):
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
return cls.objects.create(
assigned_check=check,
alert_type="check",
severity=check.alert_severity,
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
)
@classmethod
def create_task_alert(cls, task):
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
return cls.objects.create(
assigned_task=task,
alert_type="task",
severity=task.alert_severity,
message=f"{task.agent.hostname} has task: {task.name} that failed.",
hidden=True,
)
@classmethod
def create_custom_alert(cls, custom):
pass
class AlertTemplate(models.Model):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
action = models.ForeignKey(
"scripts.Script",
related_name="alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
action_timeout = models.PositiveIntegerField(default=15)
resolved_action = models.ForeignKey(
"scripts.Script",
related_name="resolved_alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
resolved_action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
resolved_action_timeout = models.PositiveIntegerField(default=15)
# overrides the global recipients
email_recipients = ArrayField(
models.CharField(max_length=100, blank=True),
null=True,
blank=True,
default=list,
)
text_recipients = ArrayField(
models.CharField(max_length=100, blank=True),
null=True,
blank=True,
default=list,
)
# overrides the from address
email_from = models.EmailField(blank=True, null=True)
# agent alert settings
agent_email_on_resolved = BooleanField(null=True, blank=True, default=False)
agent_text_on_resolved = BooleanField(null=True, blank=True, default=False)
agent_always_email = BooleanField(null=True, blank=True, default=None)
agent_always_text = BooleanField(null=True, blank=True, default=None)
agent_always_alert = BooleanField(null=True, blank=True, default=None)
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
# check alert settings
check_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
check_always_email = BooleanField(null=True, blank=True, default=None)
check_always_text = BooleanField(null=True, blank=True, default=None)
check_always_alert = BooleanField(null=True, blank=True, default=None)
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
# task alert settings
task_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
task_always_email = BooleanField(null=True, blank=True, default=None)
task_always_text = BooleanField(null=True, blank=True, default=None)
task_always_alert = BooleanField(null=True, blank=True, default=None)
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
# exclusion settings
exclude_workstations = BooleanField(null=True, blank=True, default=False)
exclude_servers = BooleanField(null=True, blank=True, default=False)
excluded_sites = models.ManyToManyField(
"clients.Site", related_name="alert_exclusions", blank=True
)
excluded_clients = models.ManyToManyField(
"clients.Client", related_name="alert_exclusions", blank=True
)
excluded_agents = models.ManyToManyField(
"agents.Agent", related_name="alert_exclusions", blank=True
)
def __str__(self):
return self.name
@property
def has_agent_settings(self) -> bool:
return (
self.agent_email_on_resolved
or self.agent_text_on_resolved
or self.agent_always_email
or self.agent_always_text
or self.agent_always_alert
or bool(self.agent_periodic_alert_days)
)
@property
def has_check_settings(self) -> bool:
return (
bool(self.check_email_alert_severity)
or bool(self.check_text_alert_severity)
or bool(self.check_dashboard_alert_severity)
or self.check_email_on_resolved
or self.check_text_on_resolved
or self.check_always_email
or self.check_always_text
or self.check_always_alert
or bool(self.check_periodic_alert_days)
)
@property
def has_task_settings(self) -> bool:
return (
bool(self.task_email_alert_severity)
or bool(self.task_text_alert_severity)
or bool(self.task_dashboard_alert_severity)
or self.task_email_on_resolved
or self.task_text_on_resolved
or self.task_always_email
or self.task_always_text
or self.task_always_alert
or bool(self.task_periodic_alert_days)
)
@property
def has_core_settings(self) -> bool:
return bool(self.email_from) or self.email_recipients or self.text_recipients
@property
def is_default_template(self) -> bool:
return self.default_alert_template.exists()

View File

@@ -1,19 +1,121 @@
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
DateTimeField,
)
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer, ReadOnlyField
from .models import Alert
from automation.serializers import PolicySerializer
from clients.serializers import ClientSerializer, SiteSerializer
from tacticalrmm.utils import get_default_timezone
from .models import Alert, AlertTemplate
class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname")
client = ReadOnlyField(source="agent.client")
site = ReadOnlyField(source="agent.site")
alert_time = DateTimeField(format="iso-8601")
hostname = SerializerMethodField(read_only=True)
client = SerializerMethodField(read_only=True)
site = SerializerMethodField(read_only=True)
alert_time = SerializerMethodField(read_only=True)
resolve_on = SerializerMethodField(read_only=True)
snoozed_until = SerializerMethodField(read_only=True)
def get_hostname(self, instance):
if instance.alert_type == "availability":
return instance.agent.hostname if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.hostname
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.hostname if instance.assigned_task else ""
)
else:
return ""
def get_client(self, instance):
if instance.alert_type == "availability":
return instance.agent.client.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.client.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.client.name
if instance.assigned_task
else ""
)
else:
return ""
def get_site(self, instance):
if instance.alert_type == "availability":
return instance.agent.site.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.site.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.site.name if instance.assigned_task else ""
)
else:
return ""
def get_alert_time(self, instance):
if instance.alert_time:
return instance.alert_time.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_resolve_on(self, instance):
if instance.resolved_on:
return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_snoozed_until(self, instance):
if instance.snooze_until:
return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
return None
class Meta:
model = Alert
fields = "__all__"
class AlertTemplateSerializer(ModelSerializer):
agent_settings = ReadOnlyField(source="has_agent_settings")
check_settings = ReadOnlyField(source="has_check_settings")
task_settings = ReadOnlyField(source="has_task_settings")
core_settings = ReadOnlyField(source="has_core_settings")
default_template = ReadOnlyField(source="is_default_template")
action_name = ReadOnlyField(source="action.name")
resolved_action_name = ReadOnlyField(source="resolved_action.name")
applied_count = SerializerMethodField()
class Meta:
model = AlertTemplate
fields = "__all__"
def get_applied_count(self, instance):
count = 0
count += instance.policies.count()
count += instance.clients.count()
count += instance.sites.count()
return count
class AlertTemplateRelationSerializer(ModelSerializer):
policies = PolicySerializer(read_only=True, many=True)
clients = ClientSerializer(read_only=True, many=True)
sites = SiteSerializer(read_only=True, many=True)
class Meta:
model = AlertTemplate
fields = "__all__"

View File

@@ -0,0 +1,14 @@
from django.utils import timezone as djangotime
from alerts.models import Alert
from tacticalrmm.celery import app
@app.task
def unsnooze_alerts() -> str:
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
snoozed=False, snooze_until=None
)
return "ok"

View File

@@ -1,3 +1,485 @@
from django.test import TestCase
from datetime import datetime, timedelta
# Create your tests here.
from django.utils import timezone as djangotime
from model_bakery import baker, seq
from core.models import CoreSettings
from tacticalrmm.test import TacticalTestCase
from .models import Alert, AlertTemplate
from .serializers import (
AlertSerializer,
AlertTemplateRelationSerializer,
AlertTemplateSerializer,
)
class TestAlertsViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_alerts(self):
url = "/alerts/alerts/"
# create check, task, and agent to test each serializer function
check = baker.make_recipe("checks.diskspace_check")
task = baker.make("autotasks.AutomatedTask")
agent = baker.make_recipe("agents.agent")
# setup data
alerts = baker.make(
"alerts.Alert",
agent=agent,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="warning",
_quantity=3,
)
baker.make(
"alerts.Alert",
assigned_check=check,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="error",
_quantity=7,
)
baker.make(
"alerts.Alert",
assigned_task=task,
snoozed=True,
snooze_until=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
_quantity=2,
)
baker.make(
"alerts.Alert",
agent=agent,
resolved=True,
resolved_on=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
_quantity=9,
)
# test top alerts for alerts icon
data = {"top": 3}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
self.assertEquals(resp.data["alerts_count"], 10)
# test filter data
# test data and result counts
data = [
{
"filter": {
"timeFilter": 30,
"snoozedFilter": True,
"resolvedFilter": False,
},
"count": 12,
},
{
"filter": {
"timeFilter": 45,
"snoozedFilter": False,
"resolvedFilter": False,
},
"count": 10,
},
{
"filter": {
"severityFilter": ["error"],
"snoozedFilter": False,
"resolvedFilter": True,
"timeFilter": 20,
},
"count": 7,
},
{
"filter": {
"clientFilter": [],
"snoozedFilter": True,
"resolvedFilter": False,
},
"count": 0,
},
{"filter": {}, "count": 21},
{"filter": {"snoozedFilter": True, "resolvedFilter": False}, "count": 12},
]
for req in data:
resp = self.client.patch(url, req["filter"], format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), req["count"])
self.check_not_authenticated("patch", url)
def test_add_alert(self):
url = "/alerts/alerts/"
agent = baker.make_recipe("agents.agent")
data = {
"alert_time": datetime.now(),
"agent": agent.id,
"severity": "warning",
"alert_type": "availability",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("post", url)
def test_get_alert(self):
# returns 404 for invalid alert pk
resp = self.client.get("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
url = f"/alerts/alerts/{alert.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertSerializer(alert)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_update_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
url = f"/alerts/alerts/{alert.pk}/"
# test resolving alert
data = {
"id": alert.pk,
"type": "resolve",
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved)
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on)
# test snoozing alert
data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed)
self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until)
# test snoozing alert without snooze_days
data = {"id": alert.pk, "type": "snooze"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test unsnoozing alert
data = {"id": alert.pk, "type": "unsnooze"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed)
self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until)
# test invalid type
data = {"id": alert.pk, "type": "invalid"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("put", url)
def test_delete_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
# test delete alert
url = f"/alerts/alerts/{alert.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(pk=alert.pk).exists())
self.check_not_authenticated("delete", url)
def test_bulk_alert_actions(self):
url = "/alerts/bulk/"
# setup data
alerts = baker.make("alerts.Alert", resolved=False, _quantity=3)
# test invalid data
data = {"bulk_action": "invalid"}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test snooze without snooze days
data = {"bulk_action": "snooze"}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test bulk snoozing alerts
data = {
"bulk_action": "snooze",
"alerts": [alert.pk for alert in alerts],
"snooze_days": "30",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(snoozed=False).exists())
# test bulk resolving alerts
data = {"bulk_action": "resolve", "alerts": [alert.pk for alert in alerts]}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(resolved=False).exists())
self.assertTrue(Alert.objects.filter(snoozed=False).exists())
def test_get_alert_templates(self):
url = "/alerts/alerttemplates/"
alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_templates, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_add_alert_template(self):
url = "/alerts/alerttemplates/"
data = {
"name": "Test Template",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("post", url)
def test_get_alert_template(self):
# returns 404 for invalid alert template pk
resp = self.client.get("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_update_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/"
# test data
data = {
"id": alert_template.pk,
"agent_email_on_resolved": True,
"agent_text_on_resolved": True,
"agent_include_desktops": True,
"agent_always_email": True,
"agent_always_text": True,
"agent_always_alert": True,
"agent_periodic_alert_days": "90",
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("put", url)
def test_delete_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
# test delete alert
url = f"/alerts/alerttemplates/{alert_template.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(AlertTemplate.objects.filter(pk=alert_template.pk).exists())
self.check_not_authenticated("delete", url)
def test_alert_template_related(self):
# setup data
alert_template = baker.make("alerts.AlertTemplate")
baker.make("clients.Client", alert_template=alert_template, _quantity=2)
baker.make("clients.Site", alert_template=alert_template, _quantity=3)
baker.make("automation.Policy", alert_template=alert_template)
core = CoreSettings.objects.first()
core.alert_template = alert_template
core.save()
url = f"/alerts/alerttemplates/{alert_template.pk}/related/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateRelationSerializer(alert_template)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data["policies"]), 1)
self.assertEqual(len(resp.data["clients"]), 2)
self.assertEqual(len(resp.data["sites"]), 3)
self.assertTrue(
AlertTemplate.objects.get(pk=alert_template.pk).is_default_template
)
class TestAlertTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_unsnooze_alert_task(self):
from alerts.tasks import unsnooze_alerts
# these will be unsnoozed whent eh function is run
not_snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=15)),
_quantity=5,
)
# these will still be snoozed after the function is run
snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=-15)),
_quantity=5,
)
unsnooze_alerts()
self.assertFalse(
Alert.objects.filter(
pk__in=[alert.pk for alert in not_snoozed], snoozed=False
).exists()
)
self.assertTrue(
Alert.objects.filter(
pk__in=[alert.pk for alert in snoozed], snoozed=False
).exists()
)
def test_agent_gets_correct_alert_template(self):
core = CoreSettings.objects.first()
# setup data
workstation = baker.make_recipe("agents.agent", monitoring_type="workstation")
server = baker.make_recipe("agents.agent", monitoring_type="server")
policy = baker.make("automation.Policy", active=True)
alert_templates = baker.make("alerts.AlertTemplate", _quantity=6)
# should be None
self.assertFalse(workstation.get_alert_template())
self.assertFalse(server.get_alert_template())
# assign first Alert Template as to a policy and apply it as default
policy.alert_template = alert_templates[0]
policy.save()
core.workstation_policy = policy
core.server_policy = policy
core.save()
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)
# assign second Alert Template to as default alert template
core.alert_template = alert_templates[1]
core.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[1].pk)
# assign third Alert Template to client
workstation.client.alert_template = alert_templates[2]
server.client.alert_template = alert_templates[2]
workstation.client.save()
server.client.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk)
# apply policy to client and should override
workstation.client.workstation_policy = policy
server.client.server_policy = policy
workstation.client.save()
server.client.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)
# assign fouth Alert Template to site
workstation.site.alert_template = alert_templates[3]
server.site.alert_template = alert_templates[3]
workstation.site.save()
server.site.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)
# apply policy to site
workstation.site.workstation_policy = policy
server.site.server_policy = policy
workstation.site.save()
server.site.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)
# apply policy to agents
workstation.policy = policy
server.policy = policy
workstation.save()
server.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)
# test disabling alert template
alert_templates[0].is_active = False
alert_templates[0].save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)
# test policy exclusions
alert_templates[3].excluded_agents.set([workstation.pk])
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)
# test workstation exclusions
alert_templates[2].exclude_workstations = True
alert_templates[2].save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)
# test server exclusions
alert_templates[3].exclude_servers = True
alert_templates[3].save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk)

View File

@@ -1,7 +1,12 @@
from django.urls import path
from . import views
urlpatterns = [
path("alerts/", views.GetAddAlerts.as_view()),
path("bulk/", views.BulkAlerts.as_view()),
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
]

View File

@@ -1,19 +1,103 @@
from datetime import datetime as dt
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from django.utils import timezone as djangotime
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from .models import Alert
from tacticalrmm.utils import notify_error
from .serializers import AlertSerializer
from .models import Alert, AlertTemplate
from .serializers import (
AlertSerializer,
AlertTemplateRelationSerializer,
AlertTemplateSerializer,
)
class GetAddAlerts(APIView):
def get(self, request):
alerts = Alert.objects.all()
def patch(self, request):
return Response(AlertSerializer(alerts, many=True).data)
# top 10 alerts for dashboard icon
if "top" in request.data.keys():
alerts = Alert.objects.filter(
resolved=False, snoozed=False, hidden=False
).order_by("alert_time")[: int(request.data["top"])]
count = Alert.objects.filter(
resolved=False, snoozed=False, hidden=False
).count()
return Response(
{
"alerts_count": count,
"alerts": AlertSerializer(alerts, many=True).data,
}
)
elif any(
key
in [
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
for key in request.data.keys()
):
clientFilter = Q()
severityFilter = Q()
timeFilter = Q()
resolvedFilter = Q()
snoozedFilter = Q()
if (
"snoozedFilter" in request.data.keys()
and not request.data["snoozedFilter"]
):
snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
if (
"resolvedFilter" in request.data.keys()
and not request.data["resolvedFilter"]
):
resolvedFilter = Q(resolved=request.data["resolvedFilter"])
if "clientFilter" in request.data.keys():
from agents.models import Agent
from clients.models import Client
clients = Client.objects.filter(
pk__in=request.data["clientFilter"]
).values_list("id")
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
"id"
)
clientFilter = Q(agent__in=agents)
if "severityFilter" in request.data.keys():
severityFilter = Q(severity__in=request.data["severityFilter"])
if "timeFilter" in request.data.keys():
timeFilter = Q(
alert_time__lte=djangotime.make_aware(dt.today()),
alert_time__gt=djangotime.make_aware(dt.today())
- djangotime.timedelta(days=int(request.data["timeFilter"])),
)
alerts = (
Alert.objects.filter(clientFilter)
.filter(severityFilter)
.filter(resolvedFilter)
.filter(snoozedFilter)
.filter(timeFilter)
)
return Response(AlertSerializer(alerts, many=True).data)
else:
alerts = Alert.objects.all()
return Response(AlertSerializer(alerts, many=True).data)
def post(self, request):
serializer = AlertSerializer(data=request.data, partial=True)
@@ -32,7 +116,40 @@ class GetUpdateDeleteAlert(APIView):
def put(self, request, pk):
alert = get_object_or_404(Alert, pk=pk)
serializer = AlertSerializer(instance=alert, data=request.data, partial=True)
data = request.data
if "type" in data.keys():
if data["type"] == "resolve":
data = {
"resolved": True,
"resolved_on": djangotime.now(),
"snoozed": False,
}
# unable to set snooze_until to none in serialzier
alert.snooze_until = None
alert.save()
elif data["type"] == "snooze":
if "snooze_days" in data.keys():
data = {
"snoozed": True,
"snooze_until": djangotime.now()
+ djangotime.timedelta(days=int(data["snooze_days"])),
}
else:
return notify_error(
"Missing 'snoozed_days' when trying to snooze alert"
)
elif data["type"] == "unsnooze":
data = {"snoozed": False}
# unable to set snooze_until to none in serialzier
alert.snooze_until = None
alert.save()
else:
return notify_error("There was an error in the request data")
serializer = AlertSerializer(instance=alert, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
@@ -42,3 +159,68 @@ class GetUpdateDeleteAlert(APIView):
Alert.objects.get(pk=pk).delete()
return Response("ok")
class BulkAlerts(APIView):
def post(self, request):
if request.data["bulk_action"] == "resolve":
Alert.objects.filter(id__in=request.data["alerts"]).update(
resolved=True,
resolved_on=djangotime.now(),
snoozed=False,
snooze_until=None,
)
return Response("ok")
elif request.data["bulk_action"] == "snooze":
if "snooze_days" in request.data.keys():
Alert.objects.filter(id__in=request.data["alerts"]).update(
snoozed=True,
snooze_until=djangotime.now()
+ djangotime.timedelta(days=int(request.data["snooze_days"])),
)
return Response("ok")
return notify_error("The request was invalid")
class GetAddAlertTemplates(APIView):
def get(self, request):
alert_templates = AlertTemplate.objects.all()
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
def post(self, request):
serializer = AlertTemplateSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteAlertTemplate(APIView):
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
return Response(AlertTemplateSerializer(alert_template).data)
def put(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
serializer = AlertTemplateSerializer(
instance=alert_template, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(AlertTemplate, pk=pk).delete()
return Response("ok")
class RelatedAlertTemplate(APIView):
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
return Response(AlertTemplateRelationSerializer(alert_template).data)

View File

@@ -1,11 +1,12 @@
import os
import json
import os
from itertools import cycle
from unittest.mock import patch
from django.conf import settings
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
from tacticalrmm.test import TacticalTestCase
class TestAPIv3(TacticalTestCase):
@@ -26,21 +27,6 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_get_mesh_info(self):
url = f"/api/v3/{self.agent.pk}/meshinfo/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url)
def test_get_winupdater(self):
url = f"/api/v3/{self.agent.agent_id}/winupdater/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url)
def test_sysinfo(self):
# TODO replace this with golang wmi sample data
@@ -59,23 +45,6 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("patch", url)
def test_hello_patch(self):
url = "/api/v3/hello/"
payload = {
"agent_id": self.agent.agent_id,
"logged_in_username": "None",
"disks": [],
}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
payload["logged_in_username"] = "Bob"
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url)
def test_checkrunner_interval(self):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json")

View File

@@ -1,19 +1,20 @@
from django.urls import path
from . import views
urlpatterns = [
path("checkin/", views.CheckIn.as_view()),
path("hello/", views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
path("meshexe/", views.MeshExe.as_view()),
path("sysinfo/", views.SysInfo.as_view()),
path("newagent/", views.NewAgent.as_view()),
path("winupdater/", views.WinUpdater.as_view()),
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
path("software/", views.Software.as_view()),
path("installer/", views.Installer.as_view()),
path("checkin/", views.CheckIn.as_view()),
path("syncmesh/", views.SyncMeshNodeID.as_view()),
path("choco/", views.Choco.as_view()),
path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
]

View File

@@ -1,84 +1,72 @@
import asyncio
import os
import requests
from loguru import logger
from packaging import version as pyver
import time
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 autotasks.models import AutomatedTask
from accounts.models import User
from winupdate.models import WinUpdatePolicy
from software.models import InstalledSoftware
from checks.serializers import CheckRunnerGetSerializerV3
from agents.models import Agent
from agents.serializers import WinAgentSerializer
from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from winupdate.serializers import ApprovedUpdateSerializer
from agents.tasks import (
agent_recovery_email_task,
agent_recovery_sms_task,
)
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializer
from checks.utils import bytes2human
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
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)
class CheckIn(APIView):
"""
The agent's checkin endpoint
patch: called every 45 to 110 seconds, handles agent updates and recovery
put: called every 5 to 10 minutes, handles basic system info
post: called once on windows service startup
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
agent.version
) or pyver.parse(request.data["version"]) == pyver.parse(
settings.LATEST_AGENT_VER
):
updated = True
agent.version = request.data["version"]
agent.last_seen = djangotime.now()
agent.save(update_fields=["version", "last_seen"])
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
last_outage = agent.agentoutages.last()
last_outage.recovery_time = djangotime.now()
last_outage.save(update_fields=["recovery_time"])
# change agent update pending status to completed if agent has just updated
if (
updated
and agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).update(status="completed")
if agent.overdue_email_alert:
agent_recovery_email_task.delay(pk=last_outage.pk)
if agent.overdue_text_alert:
agent_recovery_sms_task.delay(pk=last_outage.pk)
# 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"])
return Response(recovery.send())
# handle agent update
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
update = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
update.status = "completed"
update.save(update_fields=["status"])
return Response(update.details)
handle_agent_recovery_task.delay(pk=recovery.pk)
return Response("ok")
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
@@ -89,75 +77,13 @@ class CheckIn(APIView):
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
if "disks" in request.data.keys():
if request.data["func"] == "disks":
disks = request.data["disks"]
new = []
# python agent
if isinstance(disks, dict):
for k, v in disks.items():
new.append(v)
else:
# golang agent
for disk in disks:
tmp = {}
for k, v in disk.items():
tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"])
tmp["used"] = bytes2human(disk["used"])
tmp["free"] = bytes2human(disk["free"])
tmp["percent"] = int(disk["percent"])
new.append(tmp)
serializer.save(disks=new)
return Response("ok")
if "logged_in_username" in request.data.keys():
if request.data["logged_in_username"] != "None":
serializer.save(last_logged_in_user=request.data["logged_in_username"])
return Response("ok")
serializer.save()
return Response("ok")
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(last_seen=djangotime.now())
return Response("ok")
class Hello(APIView):
#### DEPRECATED, for agents <= 1.1.9 ####
"""
The agent's checkin endpoint
patch: called every 30 to 120 seconds
post: called on agent windows service startup
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
disks = request.data["disks"]
new = []
# python agent
if isinstance(disks, dict):
for k, v in disks.items():
new.append(v)
else:
# golang agent
for disk in disks:
tmp = {}
for k, v in disk.items():
for _, _ in disk.items():
tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"])
@@ -166,54 +92,174 @@ class Hello(APIView):
tmp["percent"] = int(disk["percent"])
new.append(tmp)
if request.data["logged_in_username"] == "None":
serializer.save(last_seen=djangotime.now(), disks=new)
else:
serializer.save(
last_seen=djangotime.now(),
disks=new,
last_logged_in_user=request.data["logged_in_username"],
serializer.is_valid(raise_exception=True)
serializer.save(disks=new)
return Response("ok")
if request.data["func"] == "loggedonuser":
if request.data["logged_in_username"] != "None":
serializer.is_valid(raise_exception=True)
serializer.save(last_logged_in_user=request.data["logged_in_username"])
return Response("ok")
if request.data["func"] == "software":
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return Response("ok")
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
# called once during tacticalagent windows service startup
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if not agent.choco_installed:
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
time.sleep(0.5)
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok")
class SyncMeshNodeID(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.mesh_node_id != request.data["nodeid"]:
agent.mesh_node_id = request.data["nodeid"]
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class Choco(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.choco_installed = request.data["installed"]
agent.save(update_fields=["choco_installed"])
return Response("ok")
class WinUpdates(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy: str = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["needs_reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
agent.delete_superseded_updates()
return Response("ok")
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
u = agent.winupdates.filter(guid=request.data["guid"]).last()
success: bool = request.data["success"]
if success:
u.result = "success"
u.downloaded = True
u.installed = True
u.date_installed = djangotime.now()
u.save(
update_fields=[
"result",
"downloaded",
"installed",
"date_installed",
]
)
else:
u.result = "failed"
u.save(update_fields=["result"])
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
last_outage = agent.agentoutages.last()
last_outage.recovery_time = djangotime.now()
last_outage.save(update_fields=["recovery_time"])
if agent.overdue_email_alert:
agent_recovery_email_task.delay(pk=last_outage.pk)
if agent.overdue_text_alert:
agent_recovery_sms_task.delay(pk=last_outage.pk)
recovery = agent.recoveryactions.filter(last_run=None).last()
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
return Response(recovery.send())
# handle agent update
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
update = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
update.status = "completed"
update.save(update_fields=["status"])
return Response(update.details)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
agent.handle_pending_actions()
agent.delete_superseded_updates()
return Response("ok")
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"]
for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists():
u = agent.winupdates.filter(guid=update["guid"]).last()
u.downloaded = update["downloaded"]
u.installed = update["installed"]
u.save(update_fields=["downloaded", "installed"])
else:
try:
kb = "KB" + update["kb_article_ids"][0]
except:
continue
WinUpdate(
agent=agent,
guid=update["guid"],
kb=kb,
title=update["title"],
installed=update["installed"],
downloaded=update["downloaded"],
description=update["description"],
severity=update["severity"],
categories=update["categories"],
category_ids=update["category_ids"],
kb_article_ids=update["kb_article_ids"],
more_info_urls=update["more_info_urls"],
support_url=update["support_url"],
revision_number=update["revision_number"],
).save()
agent.delete_superseded_updates()
# more superseded updates cleanup
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
for u in agent.winupdates.filter(
date_installed__isnull=True, result="failed"
).exclude(installed=True):
u.delete()
return Response("ok")
class SupersededWinUpdate(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = agent.winupdates.filter(guid=request.data["guid"])
for u in updates:
u.delete()
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(last_seen=djangotime.now())
return Response("ok")
@@ -232,7 +278,7 @@ class CheckRunner(APIView):
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV3(checks, many=True).data,
"checks": CheckRunnerGetSerializer(checks, many=True).data,
}
return Response(ret)
@@ -280,6 +326,8 @@ class TaskRunner(APIView):
serializer.save(last_run=djangotime.now())
new_task = AutomatedTask.objects.get(pk=task.pk)
new_task.handle_alert()
AuditLog.objects.create(
username=agent.hostname,
agent=agent.hostname,
@@ -292,74 +340,6 @@ class TaskRunner(APIView):
return Response("ok")
class WinUpdater(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent.delete_superseded_updates()
patches = agent.winupdates.filter(action="approve").exclude(installed=True)
return Response(ApprovedUpdateSerializer(patches, many=True).data)
# agent sends patch results as it's installing them
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
kb = request.data["kb"]
results = request.data["results"]
update = agent.winupdates.get(kb=kb)
if results == "error" or results == "failed":
update.result = results
update.save(update_fields=["result"])
elif results == "success":
update.result = "success"
update.downloaded = True
update.installed = True
update.date_installed = djangotime.now()
update.save(
update_fields=[
"result",
"downloaded",
"installed",
"date_installed",
]
)
elif results == "alreadyinstalled":
update.result = "success"
update.downloaded = True
update.installed = True
update.save(update_fields=["result", "downloaded", "installed"])
return Response("ok")
# agent calls this after it's finished installing all patches
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(
f"{agent.hostname} is rebooting after updates were installed."
)
return Response("ok")
class SysInfo(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
@@ -375,29 +355,6 @@ class SysInfo(APIView):
return Response("ok")
class MeshInfo(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(agent.mesh_node_id)
def patch(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
if "nodeidhex" in request.data:
# agent <= 1.1.0
nodeid = request.data["nodeidhex"]
else:
# agent >= 1.1.1
nodeid = request.data["nodeid"]
agent.mesh_node_id = nodeid
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class MeshExe(APIView):
""" Sends the mesh exe to the installer """
@@ -462,10 +419,6 @@ class NewAgent(APIView):
reload_nats()
# Generate policies for new agent
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# create agent install audit record
AuditLog.objects.create(
username=request.user,

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

@@ -0,0 +1,20 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
('automation', '0006_delete_policyexclusions'),
]
operations = [
migrations.AddField(
model_name='policy',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='policies', to='alerts.alerttemplate'),
),
]

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
@@ -9,6 +10,36 @@ class Policy(BaseAuditModel):
desc = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=False)
enforced = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="policies",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_from_policies_task
# get old policy if exists
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# generate agent checks only if active and enforced were changed
if old_policy:
if old_policy.active != self.active or old_policy.enforced != self.enforced:
generate_agent_checks_from_policies_task.delay(
policypk=self.pk,
create_tasks=True,
)
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
super(BaseAuditModel, self).delete(*args, **kwargs)
generate_agent_checks_task.delay(agents, create_tasks=True)
@property
def is_default_server_policy(self):
@@ -57,9 +88,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
@@ -122,7 +152,9 @@ class Policy(BaseAuditModel):
delete_win_task_schedule.delay(task.pk)
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
for action in agent.pendingactions.exclude(status="completed"):
for action in agent.pendingactions.filter(action_type="taskaction").exclude(
status="completed"
):
task = AutomatedTask.objects.get(pk=action.details["task_id"])
if (
task.parent_task in agent_tasks_parent_pks

View File

@@ -1,20 +1,16 @@
from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
StringRelatedField,
ReadOnlyField,
SerializerMethodField,
)
from clients.serializers import ClientSerializer, SiteSerializer
from agents.serializers import AgentHostnameSerializer
from .models import Policy
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client, Site
from clients.models import Client
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
class PolicySerializer(ModelSerializer):
class Meta:
@@ -24,15 +20,11 @@ class PolicySerializer(ModelSerializer):
class PolicyTableSerializer(ModelSerializer):
server_clients = ClientSerializer(many=True, read_only=True)
server_sites = SiteSerializer(many=True, read_only=True)
workstation_clients = ClientSerializer(many=True, read_only=True)
workstation_sites = SiteSerializer(many=True, read_only=True)
agents = AgentHostnameSerializer(many=True, read_only=True)
default_server_policy = ReadOnlyField(source="is_default_server_policy")
default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
agents_count = SerializerMethodField(read_only=True)
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
alert_template = ReadOnlyField(source="alert_template.id")
class Meta:
model = Policy
@@ -78,49 +70,16 @@ class PolicyCheckSerializer(ModelSerializer):
"assignedtask",
"text_alert",
"email_alert",
"dashboard_alert",
)
depth = 1
class AutoTasksFieldSerializer(ModelSerializer):
assigned_check = PolicyCheckSerializer(read_only=True)
script = ReadOnlyField(source="script.id")
class Meta:
model = AutomatedTask
fields = ("id", "enabled", "name", "schedule", "assigned_check")
depth = 1
class AutoTaskPolicySerializer(ModelSerializer):
autotasks = AutoTasksFieldSerializer(many=True, read_only=True)
class Meta:
model = Policy
fields = (
"id",
"name",
"autotasks",
)
depth = 2
class RelatedClientPolicySerializer(ModelSerializer):
class Meta:
model = Client
fields = ("workstation_policy", "server_policy")
depth = 1
class RelatedSitePolicySerializer(ModelSerializer):
class Meta:
model = Site
fields = ("workstation_policy", "server_policy")
depth = 1
class RelatedAgentPolicySerializer(ModelSerializer):
class Meta:
model = Agent
fields = ("policy",)
fields = "__all__"
depth = 1

View File

@@ -1,11 +1,12 @@
from automation.models import Policy
from checks.models import Check
from agents.models import Agent
from automation.models import Policy
from autotasks.models import AutomatedTask
from checks.models import Check
from tacticalrmm.celery import app
@app.task
# generates policy checks on agents affected by a policy and optionally generate automated tasks
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
policy = Policy.objects.get(pk=policypk)
@@ -21,7 +22,7 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
"pk", "monitoring_type"
)
else:
agents = policy.related_agents()
agents = policy.related_agents().only("pk")
for agent in agents:
agent.generate_checks_from_policies()
@@ -30,6 +31,17 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
@app.task
# generates policy checks on a list of agents and optionally generate automated tasks
def generate_agent_checks_task(agentpks, create_tasks=False):
for agent in Agent.objects.filter(pk__in=agentpks):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
@@ -40,6 +52,7 @@ def generate_agent_checks_by_location_task(location, mon_type, create_tasks=Fals
@app.task
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
def generate_all_agent_checks_task(mon_type, create_tasks=False):
for agent in Agent.objects.filter(monitoring_type=mon_type):
agent.generate_checks_from_policies()
@@ -49,22 +62,30 @@ def generate_all_agent_checks_task(mon_type, create_tasks=False):
@app.task
# deletes a policy managed check from all agents
def delete_policy_check_task(checkpk):
Check.objects.filter(parent_check=checkpk).delete()
@app.task
# updates policy managed check fields on agents
def update_policy_check_fields_task(checkpk):
check = Check.objects.get(pk=checkpk)
Check.objects.filter(parent_check=checkpk).update(
threshold=check.threshold,
warning_threshold=check.warning_threshold,
error_threshold=check.error_threshold,
alert_severity=check.alert_severity,
name=check.name,
disk=check.disk,
fails_b4_alert=check.fails_b4_alert,
ip=check.ip,
script=check.script,
script_args=check.script_args,
info_return_codes=check.info_return_codes,
warning_return_codes=check.warning_return_codes,
timeout=check.timeout,
pass_if_start_pending=check.pass_if_start_pending,
pass_if_svc_not_exist=check.pass_if_svc_not_exist,
@@ -79,10 +100,12 @@ def update_policy_check_fields_task(checkpk):
search_last_days=check.search_last_days,
email_alert=check.email_alert,
text_alert=check.text_alert,
dashboard_alert=check.dashboard_alert,
)
@app.task
# generates policy tasks on agents affected by a policy
def generate_agent_tasks_from_policies_task(policypk):
policy = Policy.objects.get(pk=policypk)
@@ -98,23 +121,16 @@ def generate_agent_tasks_from_policies_task(policypk):
"pk", "monitoring_type"
)
else:
agents = policy.related_agents()
agents = policy.related_agents().only("pk")
for agent in agents:
agent.generate_tasks_from_policies()
@app.task
def generate_agent_tasks_by_location_task(location, mon_type):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_tasks_from_policies()
@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)
@@ -129,13 +145,23 @@ def run_win_policy_autotask_task(task_pks):
@app.task
def update_policy_task_fields_task(taskpk, enabled):
from autotasks.models import AutomatedTask
def update_policy_task_fields_task(taskpk, update_agent=False):
from autotasks.tasks import enable_or_disable_win_task
tasks = AutomatedTask.objects.filter(parent_task=taskpk)
task = AutomatedTask.objects.get(pk=taskpk)
tasks.update(enabled=enabled)
AutomatedTask.objects.filter(parent_task=taskpk).update(
alert_severity=task.alert_severity,
email_alert=task.email_alert,
text_alert=task.text_alert,
dashboard_alert=task.dashboard_alert,
script=task.script,
script_args=task.script_args,
name=task.name,
timeout=task.timeout,
enabled=task.enabled,
)
for autotask in tasks:
enable_or_disable_win_task(autotask.pk, enabled)
if update_agent:
for task in AutomatedTask.objects.filter(parent_task=taskpk):
enable_or_disable_win_task.delay(task.pk, task.enabled)

View File

@@ -1,21 +1,20 @@
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 tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from .serializers import (
PolicyTableSerializer,
PolicySerializer,
PolicyTaskStatusSerializer,
AutoTaskPolicySerializer,
PolicyOverviewSerializer,
PolicyCheckStatusSerializer,
AutoTasksFieldSerializer,
PolicyCheckSerializer,
RelatedAgentPolicySerializer,
RelatedSitePolicySerializer,
RelatedClientPolicySerializer,
PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
@@ -91,7 +90,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("post", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_update_policy(self, mock_checks_task):
def test_update_policy(self, generate_agent_checks_from_policies_task):
# returns 404 for invalid policy pk
resp = self.client.put("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404)
@@ -110,7 +109,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
# only called if active or enforced are updated
mock_checks_task.assert_not_called()
generate_agent_checks_from_policies_task.assert_not_called()
data = {
"name": "Test Policy Update",
@@ -121,40 +120,43 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with(policypk=policy.pk, create_tasks=True)
generate_agent_checks_from_policies_task.assert_called_with(
policypk=policy.pk, create_tasks=True
)
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
def test_delete_policy(self, mock_tasks_task, mock_checks_task):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_delete_policy(self, generate_agent_checks_task):
# returns 404 for invalid policy pk
resp = self.client.delete("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404)
# setup data
policy = baker.make("automation.Policy")
site = baker.make("clients.Site")
agents = baker.make_recipe(
"agents.agent", site=site, policy=policy, _quantity=3
)
url = f"/automation/policies/{policy.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with(policypk=policy.pk)
mock_tasks_task.assert_called_with(policypk=policy.pk)
generate_agent_checks_task.assert_called_with(
[agent.pk for agent in agents], create_tasks=True
)
self.check_not_authenticated("delete", url)
def test_get_all_policy_tasks(self):
# returns 404 for invalid policy pk
resp = self.client.get("/automation/500/policyautomatedtasks/", format="json")
self.assertEqual(resp.status_code, 404)
# create policy with tasks
policy = baker.make("automation.Policy")
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/"
resp = self.client.get(url, format="json")
serializer = AutoTaskPolicySerializer(policy)
serializer = AutoTasksFieldSerializer(tasks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
@@ -180,8 +182,9 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_get_policy_check_status(self):
# set data
agent = baker.make_recipe("agents.agent")
# setup data
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site)
policy = baker.make("automation.Policy")
policy_diskcheck = baker.make_recipe("checks.diskspace_check", policy=policy)
managed_check = baker.make_recipe(
@@ -246,266 +249,6 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.generate_checks_from_policies")
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
def test_update_policy_add(
self,
mock_checks_location_task,
mock_checks_task,
):
url = f"/automation/related/"
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
agent = baker.make_recipe("agents.agent", site=site)
# test add client to policy data
client_server_payload = {
"type": "client",
"pk": agent.client.pk,
"server_policy": policy.pk,
}
client_workstation_payload = {
"type": "client",
"pk": agent.client.pk,
"workstation_policy": policy.pk,
}
# test add site to policy data
site_server_payload = {
"type": "site",
"pk": agent.site.pk,
"server_policy": policy.pk,
}
site_workstation_payload = {
"type": "site",
"pk": agent.site.pk,
"workstation_policy": policy.pk,
}
# test add agent to policy data
agent_payload = {"type": "agent", "pk": agent.pk, "policy": policy.pk}
# test client server policy add
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test client workstation policy add
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site add server policy
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site add workstation policy
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test agent add
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called()
mock_checks_task.reset_mock()
# Adding the same relations shouldn't trigger mocks
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_not_called()
# test remove client from policy data
client_server_payload = {"type": "client", "pk": client.pk, "server_policy": 0}
client_workstation_payload = {
"type": "client",
"pk": client.pk,
"workstation_policy": 0,
}
# test remove site from policy data
site_server_payload = {"type": "site", "pk": site.pk, "server_policy": 0}
site_workstation_payload = {
"type": "site",
"pk": site.pk,
"workstation_policy": 0,
}
# test remove agent from policy
agent_payload = {"type": "agent", "pk": agent.pk, "policy": 0}
# test client server policy remove
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test client workstation policy remove
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site remove server policy
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site remove workstation policy
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test agent remove
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called()
mock_checks_task.reset_mock()
# adding the same relations shouldn't trigger mocks
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_task.assert_not_called()
self.check_not_authenticated("post", url)
def test_get_relation_by_type(self):
url = f"/automation/related/"
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client", workstation_policy=policy)
site = baker.make("clients.Site", server_policy=policy)
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
client_payload = {"type": "client", "pk": client.pk}
# test add site to policy
site_payload = {"type": "site", "pk": site.pk}
# test add agent to policy
agent_payload = {"type": "agent", "pk": agent.pk}
# test client relation get
serializer = RelatedClientPolicySerializer(client)
resp = self.client.patch(url, client_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
# test site relation get
serializer = RelatedSitePolicySerializer(site)
resp = self.client.patch(url, site_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
# test agent relation get
serializer = RelatedAgentPolicySerializer(agent)
resp = self.client.patch(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
invalid_payload = {"type": "bad_type", "pk": 5}
resp = self.client.patch(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("patch", url)
def test_get_policy_task_status(self):
# policy with a task
@@ -739,8 +482,7 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
checks = self.create_checks(policy=policy)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
agent = baker.make_recipe("agents.agent", policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id)
@@ -756,16 +498,19 @@ class TestPolicyTasks(TacticalTestCase):
if check.check_type == "diskspace":
self.assertEqual(check.parent_check, checks[0].id)
self.assertEqual(check.disk, checks[0].disk)
self.assertEqual(check.threshold, checks[0].threshold)
self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "ping":
self.assertEqual(check.parent_check, checks[1].id)
self.assertEqual(check.ip, checks[1].ip)
elif check.check_type == "cpuload":
self.assertEqual(check.parent_check, checks[2].id)
self.assertEqual(check.threshold, checks[2].threshold)
self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "memory":
self.assertEqual(check.parent_check, checks[3].id)
self.assertEqual(check.threshold, checks[3].threshold)
self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "winsvc":
self.assertEqual(check.parent_check, checks[4].id)
self.assertEqual(check.svc_name, checks[4].svc_name)
@@ -801,69 +546,246 @@ class TestPolicyTasks(TacticalTestCase):
7,
)
def test_generating_agent_policy_checks_by_location(self):
from .tasks import generate_agent_checks_by_location_task
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
def test_generating_agent_policy_checks_by_location(
self, generate_agent_checks_by_location_task
):
from automation.tasks import (
generate_agent_checks_by_location_task as generate_agent_checks,
)
# setup data
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
clients = baker.make(
"clients.Client",
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.server_agent", site=sites[1])
agent2 = baker.make_recipe("agents.workstation_agent", site=sites[3])
generate_agent_checks_by_location_task(
{"site_id": sites[0].id},
"server",
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
server_agent = baker.make_recipe("agents.server_agent")
workstation_agent = baker.make_recipe("agents.workstation_agent")
# no checks should be preset on agents
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# set workstation policy on client and policy checks should be there
workstation_agent.client.workstation_policy = policy
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True,
)
# server_agent should have policy checks and the other agents should not
# make sure the checks were added
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# remove workstation policy from client
workstation_agent.client.workstation_policy = None
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure the checks were removed
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# set server policy on client and policy checks should be there
server_agent.client.server_policy = policy
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were added
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
generate_agent_checks_by_location_task(
{"site__client_id": clients[0].id},
"workstation",
# remove server policy from client
server_agent.client.server_policy = None
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
# workstation_agent should now have policy checks and the other agents should not
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# set workstation policy on site and policy checks should be there
workstation_agent.site.workstation_policy = policy
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure checks were added on workstation
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).agentchecks.count(), 0)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
def test_generating_policy_checks_for_all_agents(self):
from .tasks import generate_all_agent_checks_task
# remove workstation policy from site
workstation_agent.site.workstation_policy = None
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# set server policy on site and policy checks should be there
server_agent.site.server_policy = policy
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were added
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# remove server policy from site
server_agent.site.server_policy = None
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(
self, generate_all_agent_checks_task
):
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)
site = baker.make("clients.Site")
server_agents = baker.make_recipe("agents.server_agent", site=site, _quantity=3)
workstation_agents = baker.make_recipe(
"agents.workstation_agent", site=site, _quantity=4
)
server_agents = baker.make_recipe("agents.server_agent", _quantity=3)
workstation_agents = baker.make_recipe("agents.workstation_agent", _quantity=4)
core = CoreSettings.objects.first()
core.server_policy = policy
core.workstation_policy = policy
core.save()
generate_all_agent_checks_task("server", create_tasks=True)
generate_all_agent_checks_task.assert_called_with(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
# all servers should have 7 checks
for agent in server_agents:
@@ -872,24 +794,50 @@ class TestPolicyTasks(TacticalTestCase):
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
generate_all_agent_checks_task("workstation", create_tasks=True)
core.server_policy = None
core.workstation_policy = policy
core.save()
# all agents should have 7 checks now
generate_all_agent_checks_task.assert_any_call(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.assert_any_call(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
generate_all_checks(mon_type="workstation", create_tasks=True)
# all workstations should have 7 checks
for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
core.workstation_policy = None
core.save()
generate_all_agent_checks_task.assert_called_with(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="workstation", create_tasks=True)
# nothing should have the checks
for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
for agent in workstation_agents:
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)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_checks_from_policies()
agent = baker.make_recipe("agents.server_agent", policy=policy)
# make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
@@ -908,13 +856,12 @@ 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)
agent = baker.make_recipe("agents.server_agent", policy=policy)
agent.generate_checks_from_policies()
# make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
@@ -946,8 +893,7 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_tasks_from_policies_task(policy.id)
@@ -968,61 +914,19 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(task.parent_task, tasks[2].id)
self.assertEqual(task.name, tasks[2].name)
def test_generate_agent_tasks_by_location(self):
from .tasks import generate_agent_tasks_by_location_task
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
clients = baker.make(
"clients.Client",
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.agent", site=sites[1])
agent2 = baker.make_recipe("agents.agent", site=sites[3])
generate_agent_tasks_by_location_task({"site_id": sites[0].id}, "server")
# all servers in site1 and site2 should have 3 tasks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
generate_agent_tasks_by_location_task(
{"site__client_id": clients[0].id}, "workstation"
)
# all workstations in Default1 should have 3 tasks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 3
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_delete_policy_tasks(self, delete_win_task_schedule):
from .tasks import delete_policy_autotask_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
agent = baker.make_recipe("agents.server_agent", policy=policy)
delete_policy_autotask_task(tasks[0].id)
delete_win_task_schedule.assert_called_with(agent.autotasks.first().id)
delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id
)
@patch("autotasks.tasks.run_win_task.delay")
def test_run_policy_task(self, run_win_task):
@@ -1037,25 +941,46 @@ class TestPolicyTasks(TacticalTestCase):
for task in tasks:
run_win_task.assert_any_call(task.id)
@patch("agents.models.Agent.nats_cmd")
def test_update_policy_tasks(self, nats_cmd):
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, enable_or_disable_win_task):
from .tasks import update_policy_task_fields_task
from autotasks.models import AutomatedTask
nats_cmd.return_value = "ok"
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
)
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
agent = baker.make_recipe("agents.server_agent", policy=policy)
tasks[0].enabled = False
tasks[0].save()
update_policy_task_fields_task(tasks[0].id, enabled=False)
update_policy_task_fields_task(tasks[0].id)
enable_or_disable_win_task.assert_not_called()
self.assertFalse(AutomatedTask.objects.get(parent_task=tasks[0].id).enabled)
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
update_policy_task_fields_task(tasks[0].id, update_agent=True)
enable_or_disable_win_task.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id, False
)
@patch("agents.models.Agent.generate_tasks_from_policies")
@patch("agents.models.Agent.generate_checks_from_policies")
def test_generate_agent_checks_with_agentpks(self, generate_checks, generate_tasks):
from automation.tasks import generate_agent_checks_task
agents = baker.make_recipe("agents.agent", _quantity=5)
# reset because creating agents triggers it
generate_checks.reset_mock()
generate_tasks.reset_mock()
generate_agent_checks_task([agent.pk for agent in agents])
self.assertEquals(generate_checks.call_count, 5)
generate_tasks.assert_not_called()
generate_checks.reset_mock()
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)

View File

@@ -1,10 +1,10 @@
from django.urls import path
from . import views
urlpatterns = [
path("policies/", views.GetAddPolicies.as_view()),
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),

View File

@@ -1,39 +1,27 @@
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from .models import Policy
from agents.models import Agent
from clients.models import Client, Site
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 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,
AutoTaskPolicySerializer,
RelatedClientPolicySerializer,
RelatedSitePolicySerializer,
RelatedAgentPolicySerializer,
)
from .tasks import (
generate_agent_checks_from_policies_task,
generate_agent_checks_by_location_task,
generate_agent_tasks_from_policies_task,
run_win_policy_autotask_task,
)
from .tasks import run_win_policy_autotask_task
class GetAddPolicies(APIView):
@@ -72,29 +60,14 @@ class GetUpdateDeletePolicy(APIView):
def put(self, request, pk):
policy = get_object_or_404(Policy, pk=pk)
old_active = policy.active
old_enforced = policy.enforced
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
saved_policy = serializer.save()
# Generate agent checks only if active and enforced were changed
if saved_policy.active != old_active or saved_policy.enforced != old_enforced:
generate_agent_checks_from_policies_task.delay(
policypk=policy.pk,
create_tasks=(saved_policy.active != old_active),
)
serializer.save()
return Response("ok")
def delete(self, request, pk):
policy = get_object_or_404(Policy, pk=pk)
# delete all managed policy checks off of agents
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
generate_agent_tasks_from_policies_task.delay(policypk=policy.pk)
policy.delete()
get_object_or_404(Policy, pk=pk).delete()
return Response("ok")
@@ -103,8 +76,8 @@ class PolicyAutoTask(APIView):
# tasks associated with policy
def get(self, request, pk):
policy = get_object_or_404(Policy, pk=pk)
return Response(AutoTaskPolicySerializer(policy).data)
tasks = AutomatedTask.objects.filter(policy=pk)
return Response(AutoTasksFieldSerializer(tasks, many=True).data)
# get status of all tasks
def patch(self, request, task):
@@ -183,205 +156,12 @@ class GetRelated(APIView):
).data
response["agents"] = AgentHostnameSerializer(
policy.related_agents(),
policy.related_agents().only("pk", "hostname"),
many=True,
).data
return Response(response)
# update agents, clients, sites to policy
def post(self, request):
related_type = request.data["type"]
pk = request.data["pk"]
# workstation policy is set
if (
"workstation_policy" in request.data
and request.data["workstation_policy"] != 0
):
policy = get_object_or_404(Policy, pk=request.data["workstation_policy"])
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check and see if workstation policy changed and regenerate policies
if (
not client.workstation_policy
or client.workstation_policy
and client.workstation_policy.pk != policy.pk
):
client.workstation_policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check and see if workstation policy changed and regenerate policies
if (
not site.workstation_policy
or site.workstation_policy
and site.workstation_policy.pk != policy.pk
):
site.workstation_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
create_tasks=True,
)
# server policy is set
if "server_policy" in request.data and request.data["server_policy"] != 0:
policy = get_object_or_404(Policy, pk=request.data["server_policy"])
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check and see if server policy changed and regenerate policies
if (
not client.server_policy
or client.server_policy
and client.server_policy.pk != policy.pk
):
client.server_policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check and see if server policy changed and regenerate policies
if (
not site.server_policy
or site.server_policy
and site.server_policy.pk != policy.pk
):
site.server_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="server",
create_tasks=True,
)
# If workstation policy was cleared
if (
"workstation_policy" in request.data
and request.data["workstation_policy"] == 0
):
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check if workstation policy is set and update it to None
if client.workstation_policy:
client.workstation_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check if workstation policy is set and update it to None
if site.workstation_policy:
site.workstation_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
create_tasks=True,
)
# server policy cleared
if "server_policy" in request.data and request.data["server_policy"] == 0:
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check if server policy is set and update it to None
if client.server_policy:
client.server_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check if server policy is set and update it to None
if site.server_policy:
site.server_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.pk},
mon_type="server",
create_tasks=True,
)
# agent policies
if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk)
if "policy" in request.data and request.data["policy"] != 0:
policy = Policy.objects.get(pk=request.data["policy"])
# Check and see if policy changed and regenerate policies
if not agent.policy or agent.policy and agent.policy.pk != policy.pk:
agent.policy = policy
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
else:
if agent.policy:
agent.policy = None
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
return Response("ok")
# view to get policies set on client, site, and workstation
def patch(self, request):
related_type = request.data["type"]
# client, site, or agent pk
pk = request.data["pk"]
if related_type == "agent":
agent = Agent.objects.get(pk=pk)
return Response(RelatedAgentPolicySerializer(agent).data)
if related_type == "site":
site = Site.objects.get(pk=pk)
return Response(RelatedSitePolicySerializer(site).data)
if related_type == "client":
client = Client.objects.get(pk=pk)
return Response(RelatedClientPolicySerializer(client).data)
content = {"error": "Data was submitted incorrectly"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
class UpdatePatchPolicy(APIView):

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

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.4 on 2021-01-27 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0010_migrate_days_to_bitdays'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='alert_severity',
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='None', max_length=30, null=True),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.1.4 on 2021-01-28 04:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0011_automatedtask_alert_severity'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='email_alert',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='automatedtask',
name='email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='automatedtask',
name='text_alert',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='automatedtask',
name='text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 03:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0012_auto_20210128_0417'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='alert_severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0013_auto_20210129_0307'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-05 17:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0014_automatedtask_dashboard_alert'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='automatedtask',
name='resolved_text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-02-05 21:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0015_auto_20210205_1728'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='status',
field=models.CharField(choices=[('passing', 'Passing'), ('failing', 'Failing'), ('pending', 'Pending')], default='pending', max_length=30),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.4 on 2021-02-10 15:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0016_automatedtask_status'),
]
operations = [
migrations.RemoveField(
model_name='automatedtask',
name='email_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='resolved_email_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='resolved_text_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='text_sent',
),
]

View File

@@ -1,14 +1,21 @@
import pytz
import datetime as dt
import random
import string
import datetime as dt
from django.db import models
import pytz
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import DateTimeField
from django.utils import timezone as djangotime
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)
RUN_TIME_DAY_CHOICES = [
(0, "Monday"),
(1, "Tuesday"),
@@ -32,6 +39,12 @@ SYNC_STATUS_CHOICES = [
("pendingdeletion", "Pending Deletion on Agent"),
]
TASK_STATUS_CHOICES = [
("passing", "Passing"),
("failing", "Failing"),
("pending", "Pending"),
]
class AutomatedTask(BaseAuditModel):
agent = models.ForeignKey(
@@ -93,9 +106,18 @@ class AutomatedTask(BaseAuditModel):
execution_time = models.CharField(max_length=100, default="0.0000")
last_run = models.DateTimeField(null=True, blank=True)
enabled = models.BooleanField(default=True)
status = models.CharField(
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
)
sync_status = models.CharField(
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
)
alert_severity = models.CharField(
max_length=30, choices=SEVERITY_CHOICES, default="info"
)
email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False)
dashboard_alert = models.BooleanField(default=False)
def __str__(self):
return self.name
@@ -140,22 +162,49 @@ class AutomatedTask(BaseAuditModel):
def create_policy_task(self, agent=None, policy=None):
from .tasks import create_win_task_schedule
# 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
assigned_check = None
# get correct assigned check to task if set
if agent and self.assigned_check:
assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.pk)
# check if there is a matching check on the agent
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
assigned_check = agent.agentchecks.filter(
parent_check=self.assigned_check.pk
).first()
# check was overriden by agent and we need to use that agents check
else:
if agent.agentchecks.filter(
check_type=self.assigned_check.check_type, overriden_by_policy=True
).exists():
assigned_check = agent.agentchecks.filter(
check_type=self.assigned_check.check_type,
overriden_by_policy=True,
).first()
elif policy and self.assigned_check:
assigned_check = policy.policychecks.get(name=self.assigned_check.name)
if policy.policychecks.filter(name=self.assigned_check.name).exists():
assigned_check = policy.policychecks.filter(
name=self.assigned_check.name
).first()
else:
assigned_check = policy.policychecks.filter(
check_type=self.assigned_check.check_type
).first()
task = AutomatedTask.objects.create(
agent=agent,
policy=policy,
managed_by_policy=bool(agent),
parent_task=(self.pk if agent else None),
alert_severity=self.alert_severity,
email_alert=self.email_alert,
text_alert=self.text_alert,
dashboard_alert=self.dashboard_alert,
script=self.script,
script_args=self.script_args,
assigned_check=assigned_check,
@@ -172,3 +221,215 @@ class AutomatedTask(BaseAuditModel):
)
create_win_task_schedule.delay(task.pk)
def handle_alert(self) -> None:
from alerts.models import Alert
from autotasks.tasks import (
handle_resolved_task_email_alert,
handle_resolved_task_sms_alert,
handle_task_email_alert,
handle_task_sms_alert,
)
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"
else:
subject = f"{self} Failed"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, 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"
else:
subject = f"{self} Failed"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=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 = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=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)

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):
@@ -14,6 +13,24 @@ class TaskSerializer(serializers.ModelSerializer):
assigned_check = CheckSerializer(read_only=True)
schedule = serializers.ReadOnlyField()
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
else:
alert_template = None
if not alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.task_always_email,
"always_text": alert_template.task_always_text,
"always_alert": alert_template.task_always_alert,
}
class Meta:
model = AutomatedTask

View File

@@ -1,14 +1,19 @@
import asyncio
import datetime as dt
from loguru import logger
from tacticalrmm.celery import app
from django.conf import settings
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 .models import AutomatedTask
from logs.models import PendingAction
from tacticalrmm.celery import app
from .models import AutomatedTask
logger.configure(**settings.LOG_CONFIG)
@@ -243,3 +248,85 @@ def remove_orphaned_win_tasks(agentpk):
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
@app.task
def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.email_sent:
sleep(random.randint(1, 10))
alert.assigned_task.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
else:
if alert_interval:
# send an email only if the last email sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta:
sleep(random.randint(1, 10))
alert.assigned_task.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
return "ok"
@app.task
def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.sms_sent:
sleep(random.randint(1, 3))
alert.assigned_task.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
else:
if alert_interval:
# send a text only if the last text sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta:
sleep(random.randint(1, 3))
alert.assigned_task.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
return "ok"
@app.task
def handle_resolved_task_sms_alert(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.resolved_sms_sent:
sleep(random.randint(1, 3))
alert.assigned_task.send_resolved_sms()
alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task
def handle_resolved_task_email_alert(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.resolved_email_sent:
sleep(random.randint(1, 10))
alert.assigned_task.send_resolved_email()
alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"])
return "ok"

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):
@@ -150,7 +151,9 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
update_policy_task_fields_task.assert_called_with(policy_task.id, True)
update_policy_task_fields_task.assert_called_with(
policy_task.id, update_agent=True
)
self.check_not_authenticated("patch", url)

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,11 +72,25 @@ 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)
def put(self, request, pk):
from automation.tasks import update_policy_task_fields_task
task = get_object_or_404(AutomatedTask, pk=pk)
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
if task.policy:
update_policy_task_fields_task.delay(task.pk)
return Response("ok")
def patch(self, request, pk):
from automation.tasks import update_policy_task_fields_task
@@ -93,7 +103,7 @@ class AutoTask(APIView):
enable_or_disable_win_task.delay(pk=task.pk, action=action)
else:
update_policy_task_fields_task.delay(task.pk, action)
update_policy_task_fields_task.delay(task.pk, update_agent=True)
task.enabled = action
task.save(update_fields=["enabled"])

View File

@@ -1,15 +1,20 @@
from .models import Check
from model_bakery.recipe import Recipe, seq
from model_bakery.recipe import Recipe
check = Recipe(Check)
check = Recipe("checks.Check")
diskspace_check = check.extend(check_type="diskspace", disk="C:", threshold=75)
diskspace_check = check.extend(
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
)
cpuload_check = check.extend(check_type="cpuload", threshold=75)
cpuload_check = check.extend(
check_type="cpuload", warning_threshold=30, error_threshold=75
)
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
memory_check = check.extend(check_type="memory", threshold=75)
memory_check = check.extend(
check_type="memory", warning_threshold=30, error_threshold=75
)
winsvc_check = check.extend(
check_type="winsvc",

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,43 @@
# Generated by Django 3.1.4 on 2021-01-23 01:49
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0015_auto_20210110_1808'),
]
operations = [
migrations.RemoveField(
model_name='check',
name='threshold',
),
migrations.AddField(
model_name='check',
name='alert_severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15),
),
migrations.AddField(
model_name='check',
name='error_threshold',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='check',
name='info_return_codes',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
),
migrations.AddField(
model_name='check',
name='warning_return_codes',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
),
migrations.AddField(
model_name='check',
name='warning_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0016_auto_20210123_0149'),
]
operations = [
migrations.AddField(
model_name='check',
name='dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-02-05 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0017_check_dashboard_alert'),
]
operations = [
migrations.AlterField(
model_name='check',
name='alert_severity',
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15, null=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-05 17:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0018_auto_20210205_1647'),
]
operations = [
migrations.AddField(
model_name='check',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='check',
name='resolved_text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.4 on 2021-02-10 15:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('checks', '0019_auto_20210205_1728'),
]
operations = [
migrations.RemoveField(
model_name='check',
name='email_sent',
),
migrations.RemoveField(
model_name='check',
name='resolved_email_sent',
),
migrations.RemoveField(
model_name='check',
name='resolved_text_sent',
),
migrations.RemoveField(
model_name='check',
name='text_sent',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.4 on 2021-02-12 14:29
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0020_auto_20210210_1512'),
]
operations = [
migrations.AlterField(
model_name='check',
name='error_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
),
migrations.AlterField(
model_name='check',
name='warning_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
),
]

View File

@@ -1,21 +1,33 @@
import asyncio
import string
import os
import json
import pytz
from statistics import mean, mode
import os
import string
from statistics import mean
from typing import Any, List, Union
from django.db import models
import pytz
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import timezone as djangotime
from loguru import logger
from rest_framework.fields import JSONField
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
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
logger.configure(**settings.LOG_CONFIG)
CHECK_TYPE_CHOICES = [
("diskspace", "Disk Space Check"),
("ping", "Ping Check"),
@@ -84,18 +96,34 @@ class Check(BaseAuditModel):
last_run = models.DateTimeField(null=True, blank=True)
email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False)
dashboard_alert = models.BooleanField(default=False)
fails_b4_alert = models.PositiveIntegerField(default=1)
fail_count = models.PositiveIntegerField(default=0)
email_sent = models.DateTimeField(null=True, blank=True)
text_sent = models.DateTimeField(null=True, blank=True)
outage_history = models.JSONField(null=True, blank=True) # store
extra_details = models.JSONField(null=True, blank=True)
# check specific fields
# for eventlog, script, ip, and service alert severity
alert_severity = models.CharField(
max_length=15,
choices=SEVERITY_CHOICES,
default="warning",
null=True,
blank=True,
)
# threshold percent for diskspace, cpuload or memory check
threshold = models.PositiveIntegerField(
null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(99)]
error_threshold = models.PositiveIntegerField(
validators=[MinValueValidator(0), MaxValueValidator(99)],
null=True,
blank=True,
default=0,
)
warning_threshold = models.PositiveIntegerField(
null=True,
blank=True,
validators=[MinValueValidator(0), MaxValueValidator(99)],
default=0,
)
# diskcheck i.e C:, D: etc
disk = models.CharField(max_length=2, null=True, blank=True)
@@ -115,6 +143,18 @@ class Check(BaseAuditModel):
blank=True,
default=list,
)
info_return_codes = ArrayField(
models.PositiveIntegerField(),
null=True,
blank=True,
default=list,
)
warning_return_codes = ArrayField(
models.PositiveIntegerField(),
null=True,
blank=True,
default=list,
)
timeout = models.PositiveIntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
@@ -159,11 +199,25 @@ class Check(BaseAuditModel):
@property
def readable_desc(self):
if self.check_type == "diskspace":
return f"{self.get_check_type_display()}: Drive {self.disk} < {self.threshold}%"
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
elif self.check_type == "ping":
return f"{self.get_check_type_display()}: {self.name}"
elif self.check_type == "cpuload" or self.check_type == "memory":
return f"{self.get_check_type_display()} > {self.threshold}%"
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()} > {text}"
elif self.check_type == "winsvc":
return f"{self.get_check_type_display()}: {self.svc_display_name}"
elif self.check_type == "eventlog":
@@ -188,15 +242,13 @@ class Check(BaseAuditModel):
return self.last_run
@property
def non_editable_fields(self):
def non_editable_fields(self) -> List[str]:
return [
"check_type",
"status",
"more_info",
"last_run",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
@@ -215,10 +267,148 @@ class Check(BaseAuditModel):
"modified_time",
]
def add_check_history(self, value, more_info=None):
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
# 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,
)
# 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):
# cpuload or mem checks
if self.check_type == "cpuload" or self.check_type == "memory":
@@ -231,8 +421,12 @@ class Check(BaseAuditModel):
avg = int(mean(self.history))
if avg > self.threshold:
if self.error_threshold and avg > self.error_threshold:
self.status = "failing"
self.alert_severity = "error"
elif self.warning_threshold and avg > self.warning_threshold:
self.status = "failing"
self.alert_severity = "warning"
else:
self.status = "passing"
@@ -246,17 +440,26 @@ class Check(BaseAuditModel):
total = bytes2human(data["total"])
free = bytes2human(data["free"])
if (100 - percent_used) < self.threshold:
if self.error_threshold and (100 - percent_used) < self.error_threshold:
self.status = "failing"
self.alert_severity = "error"
elif (
self.warning_threshold
and (100 - percent_used) < self.warning_threshold
):
self.status = "failing"
self.alert_severity = "warning"
else:
self.status = "passing"
self.more_info = f"Total: {total}B, Free: {free}B"
# add check history
self.add_check_history(percent_used)
self.add_check_history(100 - percent_used)
else:
self.status = "failing"
self.alert_severity = "error"
self.more_info = f"Disk {self.disk} does not exist"
self.save(update_fields=["more_info"])
@@ -273,8 +476,15 @@ class Check(BaseAuditModel):
# golang agent
self.execution_time = "{:.4f}".format(data["runtime"])
if data["retcode"] != 0:
if data["retcode"] in self.info_return_codes:
self.alert_severity = "info"
self.status = "failing"
elif data["retcode"] in self.warning_return_codes:
self.alert_severity = "warning"
self.status = "failing"
elif data["retcode"] != 0:
self.status = "failing"
self.alert_severity = "error"
else:
self.status = "passing"
@@ -428,59 +638,16 @@ class Check(BaseAuditModel):
# handle status
if self.status == "failing":
self.fail_count += 1
self.save(update_fields=["status", "fail_count"])
self.save(update_fields=["status", "fail_count", "alert_severity"])
elif self.status == "passing":
if self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["status", "fail_count"])
else:
self.save(update_fields=["status"])
self.fail_count = 0
self.save(update_fields=["status", "fail_count", "alert_severity"])
if self.fail_count >= self.fails_b4_alert:
if self.email_alert:
handle_check_email_alert_task.delay(self.pk)
if self.text_alert:
handle_check_sms_alert_task.delay(self.pk)
self.handle_alert()
return self.status
def handle_check(self, data):
if self.check_type != "cpuload" and self.check_type != "memory":
if data["status"] == "passing" and self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["fail_count"])
elif data["status"] == "failing":
self.fail_count += 1
self.save(update_fields=["fail_count"])
else:
self.history.append(data["percent"])
if len(self.history) > 15:
self.history = self.history[-15:]
self.save(update_fields=["history"])
avg = int(mean(self.history))
if avg > self.threshold:
self.status = "failing"
self.fail_count += 1
self.save(update_fields=["status", "fail_count"])
else:
self.status = "passing"
if self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["status", "fail_count"])
else:
self.save(update_fields=["status"])
if self.email_alert and self.fail_count >= self.fails_b4_alert:
handle_check_email_alert_task.delay(self.pk)
@staticmethod
def serialize(check):
# serializes the check and returns json
@@ -514,17 +681,22 @@ class Check(BaseAuditModel):
managed_by_policy=bool(agent),
parent_check=(self.pk if agent else None),
name=self.name,
alert_severity=self.alert_severity,
check_type=self.check_type,
email_alert=self.email_alert,
dashboard_alert=self.dashboard_alert,
text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details,
threshold=self.threshold,
error_threshold=self.error_threshold,
warning_threshold=self.warning_threshold,
disk=self.disk,
ip=self.ip,
script=self.script,
script_args=self.script_args,
timeout=self.timeout,
info_return_codes=self.info_return_codes,
warning_return_codes=self.warning_return_codes,
svc_name=self.svc_name,
svc_display_name=self.svc_display_name,
pass_if_start_pending=self.pass_if_start_pending,
@@ -566,19 +738,27 @@ 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"
else:
subject = f"{self} Failed"
if self.check_type == "diskspace":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
percent_used = [
d["percent"] for d in self.agent.disks if d["device"] == self.disk
][0]
percent_free = 100 - percent_used
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%"
body = subject + f" - Free: {percent_free}%, {text}"
elif self.check_type == "script":
@@ -592,26 +772,29 @@ class Check(BaseAuditModel):
body = self.more_info
elif self.check_type == "cpuload" or self.check_type == "memory":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
avg = int(mean(self.history))
if self.check_type == "cpuload":
body = (
subject
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
)
body = subject + f" - Average CPU utilization: {avg}%, {text}"
elif self.check_type == "memory":
body = (
subject
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
)
body = subject + f" - Average memory usage: {avg}%, {text}"
elif self.check_type == "winsvc":
status = list(
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
)[0]["status"]
try:
status = list(
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
)[0]["status"]
# catch services that don't exist if policy check
except:
status = "Unknown"
body = subject + f" - Status: {status.upper()}"
@@ -637,11 +820,13 @@ class Check(BaseAuditModel):
except:
continue
CORE.send_mail(subject, body)
CORE.send_mail(subject, body, alert_template=alert_template)
def send_sms(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"
@@ -649,27 +834,33 @@ class Check(BaseAuditModel):
subject = f"{self} Failed"
if self.check_type == "diskspace":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
percent_used = [
d["percent"] for d in self.agent.disks if d["device"] == self.disk
][0]
percent_free = 100 - percent_used
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%"
body = subject + f" - Free: {percent_free}%, {text}"
elif self.check_type == "script":
body = subject + f" - Return code: {self.retcode}"
elif self.check_type == "ping":
body = subject
elif self.check_type == "cpuload" or self.check_type == "memory":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
avg = int(mean(self.history))
if self.check_type == "cpuload":
body = (
subject
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
)
body = subject + f" - Average CPU utilization: {avg}%, {text}"
elif self.check_type == "memory":
body = (
subject
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
)
body = subject + f" - Average memory usage: {avg}%, {text}"
elif self.check_type == "winsvc":
status = list(
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
@@ -678,7 +869,21 @@ class Check(BaseAuditModel):
elif self.check_type == "eventlog":
body = subject
CORE.send_sms(body)
CORE.send_sms(body, alert_template=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)
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)
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):
@@ -20,6 +21,23 @@ class CheckSerializer(serializers.ModelSerializer):
assigned_task = serializers.SerializerMethodField()
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
history_info = serializers.ReadOnlyField()
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
else:
alert_template = None
if not alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.check_always_email,
"always_text": alert_template.check_always_text,
"always_alert": alert_template.check_always_alert,
}
## Change to return only array of tasks after 9/25/2020
def get_assigned_task(self, obj):
@@ -40,19 +58,35 @@ class CheckSerializer(serializers.ModelSerializer):
check_type = val["check_type"]
except KeyError:
return val
# disk checks
# make sure no duplicate diskchecks exist for an agent/policy
if check_type == "diskspace" and not self.instance: # only on create
checks = (
Check.objects.filter(**self.context)
.filter(check_type="diskspace")
.exclude(managed_by_policy=True)
)
for check in checks:
if val["disk"] in check.disk:
raise serializers.ValidationError(
f"A disk check for Drive {val['disk']} already exists!"
)
if check_type == "diskspace":
if not self.instance: # only on create
checks = (
Check.objects.filter(**self.context)
.filter(check_type="diskspace")
.exclude(managed_by_policy=True)
)
for check in checks:
if val["disk"] in check.disk:
raise serializers.ValidationError(
f"A disk check for Drive {val['disk']} already exists!"
)
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] < val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be greater than Error Threshold"
)
# ping checks
if check_type == "ping":
@@ -75,6 +109,20 @@ class CheckSerializer(serializers.ModelSerializer):
"A cpuload check for this agent already exists"
)
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] > val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be less than Error Threshold"
)
if check_type == "memory" and not self.instance:
if (
Check.objects.filter(**self.context, check_type="memory")
@@ -85,6 +133,20 @@ class CheckSerializer(serializers.ModelSerializer):
"A memory check for this agent already exists"
)
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] > val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be less than Error Threshold"
)
return val
@@ -95,101 +157,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer):
# for the windows agent
# only send data needed for agent to run a check
assigned_task = serializers.SerializerMethodField()
script = ScriptSerializer(read_only=True)
def get_assigned_task(self, obj):
if obj.assignedtask.exists():
# this will not break agents on version 0.10.2 or lower
# newer agents once released will properly handle multiple tasks assigned to a check
task = obj.assignedtask.first()
return AssignedTaskCheckRunnerField(task).data
class Meta:
model = Check
exclude = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV2(serializers.ModelSerializer):
# for the windows __python__ agent
# only send data needed for agent to run a check
assigned_tasks = serializers.SerializerMethodField()
script = ScriptSerializer(read_only=True)
def get_assigned_tasks(self, obj):
if obj.assignedtask.exists():
tasks = obj.assignedtask.all()
return AssignedTaskCheckRunnerField(tasks, many=True).data
class Meta:
model = Check
exclude = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
# for the windows __golang__ agent
# only send data needed for agent to run a check
# the difference here is in the script serializer
# script checks no longer rely on salt and are executed directly by the go agent
assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True)
@@ -212,8 +180,6 @@ class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",

View File

@@ -1,57 +1,91 @@
import datetime as dt
import random
from time import sleep
from typing import Union
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
from django.utils import timezone as djangotime
@app.task
def handle_check_email_alert_task(pk):
from .models import Check
def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
check = Check.objects.get(pk=pk)
alert = Alert.objects.get(pk=pk)
if not check.agent.maintenance_mode:
# first time sending email
if not check.email_sent:
sleep(random.randint(1, 10))
check.send_email()
check.email_sent = djangotime.now()
check.save(update_fields=["email_sent"])
else:
# send an email only if the last email sent is older than 24 hours
delta = djangotime.now() - dt.timedelta(hours=24)
if check.email_sent < delta:
# first time sending email
if not alert.email_sent:
sleep(random.randint(1, 10))
alert.assigned_check.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
else:
if alert_interval:
# send an email only if the last email sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta:
sleep(random.randint(1, 10))
check.send_email()
check.email_sent = djangotime.now()
check.save(update_fields=["email_sent"])
alert.assigned_check.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
return "ok"
@app.task
def handle_check_sms_alert_task(pk):
from .models import Check
def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
check = Check.objects.get(pk=pk)
alert = Alert.objects.get(pk=pk)
if not check.agent.maintenance_mode:
# first time sending text
if not check.text_sent:
sleep(random.randint(1, 3))
check.send_sms()
check.text_sent = djangotime.now()
check.save(update_fields=["text_sent"])
else:
# first time sending text
if not alert.sms_sent:
sleep(random.randint(1, 3))
alert.assigned_check.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
else:
if alert_interval:
# send a text only if the last text sent is older than 24 hours
delta = djangotime.now() - dt.timedelta(hours=24)
if check.text_sent < delta:
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta:
sleep(random.randint(1, 3))
check.send_sms()
check.text_sent = djangotime.now()
check.save(update_fields=["text_sent"])
alert.assigned_check.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
return "ok"
@app.task
def handle_resolved_check_sms_alert_task(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.resolved_sms_sent:
sleep(random.randint(1, 3))
alert.assigned_check.send_resolved_sms()
alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task
def handle_resolved_check_email_alert_task(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.resolved_email_sent:
sleep(random.randint(1, 10))
alert.assigned_check.send_resolved_email()
alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"])
return "ok"

View File

@@ -1,10 +1,12 @@
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
from django.utils import timezone as djangotime
from model_bakery import baker
from itertools import cycle
from .serializers import CheckSerializer
class TestCheckViews(TacticalTestCase):
@@ -23,7 +25,7 @@ class TestCheckViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("post", url)
self.check_not_authenticated("get", url)
def test_add_disk_check(self):
# setup data
@@ -36,7 +38,8 @@ class TestCheckViews(TacticalTestCase):
"check": {
"check_type": "diskspace",
"disk": "C:",
"threshold": 55,
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
}
@@ -50,7 +53,8 @@ class TestCheckViews(TacticalTestCase):
"check": {
"check_type": "diskspace",
"disk": "C:",
"threshold": 55,
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
}
@@ -58,6 +62,38 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error and warning threshold are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error is greater than warning threshold
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 50,
"warning_threshold": 30,
"fails_b4_alert": 3,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_cpuload_check(self):
url = "/checks/checks/"
agent = baker.make_recipe("agents.agent")
@@ -65,7 +101,8 @@ class TestCheckViews(TacticalTestCase):
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"threshold": 66,
"error_threshold": 66,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
@@ -73,7 +110,7 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
payload["threshold"] = 87
payload["error_threshold"] = 87
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(
@@ -81,6 +118,36 @@ class TestCheckViews(TacticalTestCase):
"A cpuload check for this agent already exists",
)
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_memory_check(self):
url = "/checks/checks/"
agent = baker.make_recipe("agents.agent")
@@ -88,7 +155,8 @@ class TestCheckViews(TacticalTestCase):
"pk": agent.pk,
"check": {
"check_type": "memory",
"threshold": 78,
"error_threshold": 78,
"warning_threshold": 0,
"fails_b4_alert": 1,
},
}
@@ -96,7 +164,7 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
payload["threshold"] = 55
payload["error_threshold"] = 55
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(
@@ -104,6 +172,34 @@ class TestCheckViews(TacticalTestCase):
"A memory check for this agent already exists",
)
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
def test_get_policy_disk_check(self):
# setup data
policy = baker.make("automation.Policy")
@@ -129,11 +225,37 @@ class TestCheckViews(TacticalTestCase):
"check": {
"check_type": "diskspace",
"disk": "M:",
"threshold": 86,
"error_threshold": 86,
"warning_threshold": 0,
"fails_b4_alert": 2,
},
}
# should fail because both error and warning thresholds are 0
invalid_payload = {
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because warning is less than error
invalid_payload = {
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"error_threshold": 80,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, valid_payload, format="json")
self.assertEqual(resp.status_code, 200)
@@ -143,7 +265,8 @@ class TestCheckViews(TacticalTestCase):
"check": {
"check_type": "diskspace",
"disk": "M:",
"threshold": 34,
"error_threshold": 34,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
@@ -184,6 +307,48 @@ class TestCheckViews(TacticalTestCase):
self.check_not_authenticated("patch", url_a)
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_old.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
nats_cmd.reset_mock()
nats_cmd.return_value = "busy"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}")
nats_cmd.reset_mock()
nats_cmd.return_value = "ok"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}")
nats_cmd.reset_mock()
nats_cmd.return_value = "timeout"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), "Unable to contact the agent")
self.check_not_authenticated("get", url)
def test_get_check_history(self):
# setup data
agent = baker.make_recipe("agents.agent")

View File

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

View File

@@ -1,30 +1,26 @@
import asyncio
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):
@@ -168,8 +164,17 @@ def run_checks(request, pk):
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(agent.hostname)
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
if r == "busy":
return notify_error(f"Checks are already running on {agent.hostname}")
elif r == "ok":
return Response(f"Checks will now be re-run on {agent.hostname}")
else:
return notify_error("Unable to contact the agent")
else:
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(f"Checks will now be re-run on {agent.hostname}")
@api_view()

View File

@@ -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

@@ -0,0 +1,25 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
('clients', '0008_auto_20201103_1430'),
]
operations = [
migrations.AddField(
model_name='client',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='alerts.alerttemplate'),
),
migrations.AddField(
model_name='site',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='alerts.alerttemplate'),
),
]

View File

@@ -23,6 +23,36 @@ class Client(BaseAuditModel):
blank=True,
on_delete=models.SET_NULL,
)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="clients",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kw):
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_client and old_client.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_client and old_client.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
class Meta:
ordering = ("name",)
@@ -45,6 +75,7 @@ class Client(BaseAuditModel):
"overdue_text_alert",
"last_seen",
"overdue_time",
"offline_time",
)
.filter(site__client=self)
.prefetch_related("agentchecks")
@@ -87,6 +118,36 @@ class Site(BaseAuditModel):
blank=True,
on_delete=models.SET_NULL,
)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="sites",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kw):
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_site and old_site.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_site and old_site.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
class Meta:
ordering = ("name",)
@@ -107,6 +168,7 @@ class Site(BaseAuditModel):
"overdue_text_alert",
"last_seen",
"overdue_time",
"offline_time",
)
.filter(site=self)
.prefetch_related("agentchecks")

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):
@@ -10,7 +11,7 @@ class SiteSerializer(ModelSerializer):
fields = "__all__"
def validate(self, val):
if "|" in val["name"]:
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Site name cannot contain the | character")
if self.context:
@@ -36,7 +37,7 @@ class ClientSerializer(ModelSerializer):
if len(self.context["site"]) > 255:
raise ValidationError("Site name too long")
if "|" in val["name"]:
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Client name cannot contain the | character")
return val

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,35 +1,30 @@
import pytz
import re
import os
import uuid
import subprocess
import datetime as dt
import os
import re
import subprocess
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 .models import Client, Deployment, Site
from .serializers import (
ClientSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteSerializer,
)
class GetAddClients(APIView):
def get(self, request):
@@ -61,7 +56,8 @@ class GetAddClients(APIView):
class GetUpdateDeleteClient(APIView):
def put(self, request, pk):
client = get_object_or_404(Client, pk=pk)
serializer = ClientSerializer(data=request.data, instance=client)
serializer = ClientSerializer(data=request.data, instance=client, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
@@ -106,7 +102,7 @@ class GetUpdateDeleteSite(APIView):
def put(self, request, pk):
site = get_object_or_404(Site, pk=pk)
serializer = SiteSerializer(instance=site, data=request.data)
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
@@ -223,7 +219,7 @@ class GenerateAgent(APIView):
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-X 'main.Inno={inno}'",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={d.client.pk}'",
f"-X 'main.Site={d.site.pk}'",

View File

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

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