Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d05c1205 | ||
|
|
2973e0559a | ||
|
|
ec27288dcf | ||
|
|
f92e5c7093 | ||
|
|
7c67155c49 | ||
|
|
b102cd4652 | ||
|
|
67f9a48c37 | ||
|
|
a0c8a1ee65 | ||
|
|
7e7d272b06 | ||
|
|
3c642240ae | ||
|
|
b5157fcaf1 | ||
|
|
d1cb42f1bc | ||
|
|
84cde1a16a | ||
|
|
877f5db1ce | ||
|
|
787164e245 | ||
|
|
d77fc5e7c5 | ||
|
|
cca39a67d6 | ||
|
|
a6c9a0431a | ||
|
|
729a80a639 | ||
|
|
31cb3001f6 | ||
|
|
5d0f54a329 | ||
|
|
c8c3f5b5b7 | ||
|
|
ba473ed75a | ||
|
|
7236fd59f8 | ||
|
|
9471e8f1fd | ||
|
|
a2d39b51bb | ||
|
|
2920934b55 | ||
|
|
3f709d448e | ||
|
|
b79f66183f | ||
|
|
8672f57e55 | ||
|
|
1e99c82351 | ||
|
|
1a2ff851f3 | ||
|
|
f1c27c3959 | ||
|
|
b30dac0f15 | ||
|
|
cc79e5cdaf | ||
|
|
d9a3b2f2cb | ||
|
|
479b528d09 | ||
|
|
461fb84fb9 | ||
|
|
bd7685e3fa | ||
|
|
cd98cb64b3 | ||
|
|
0f32a3ec24 | ||
|
|
ca446cac87 | ||
|
|
6ea907ffda | ||
|
|
5287baa70d | ||
|
|
25935fec84 | ||
|
|
e855a063ff | ||
|
|
c726b8c9f0 | ||
|
|
13cb99290e | ||
|
|
cea9413fd1 | ||
|
|
1432853b39 | ||
|
|
6d6c2b86e8 | ||
|
|
77b1d964b5 | ||
|
|
549936fc09 | ||
|
|
c9c32f09c5 | ||
|
|
77f7778d4a | ||
|
|
84b6be9364 | ||
|
|
1e43b55804 | ||
|
|
ba9bdaae0a | ||
|
|
7dfd7bde8e | ||
|
|
5e6c4161d0 | ||
|
|
d75d56dfc9 | ||
|
|
1d9d350091 | ||
|
|
5744053c6f | ||
|
|
65589b6ca2 | ||
|
|
e03a9d1137 | ||
|
|
29f80f2276 | ||
|
|
a9b74aa69b | ||
|
|
63ebfd3210 | ||
|
|
87fa5ff7a6 | ||
|
|
b686b53a9c | ||
|
|
258261dc64 | ||
|
|
9af5c9ead9 | ||
|
|
382654188c | ||
|
|
fa1df082b7 | ||
|
|
5c227d8f80 | ||
|
|
81dabdbfb7 | ||
|
|
91f89f5a33 | ||
|
|
9f92746aa0 | ||
|
|
5d6e6f9441 | ||
|
|
01395a2726 | ||
|
|
465d75c65d | ||
|
|
4634f8927e | ||
|
|
74a287f9fe | ||
|
|
7ff6c79835 | ||
|
|
3629982237 | ||
|
|
ddb610f1bc | ||
|
|
f899905d27 | ||
|
|
3e4531b5c5 | ||
|
|
a9e189e51d | ||
|
|
58ba08a8f3 | ||
|
|
9078ff27d8 | ||
|
|
6f43e61c24 | ||
|
|
4be0d3f212 | ||
|
|
00e47e5a27 | ||
|
|
152e145b32 | ||
|
|
54e55e8f57 | ||
|
|
05b8707f9e | ||
|
|
543e952023 | ||
|
|
6e5f40ea06 | ||
|
|
bbafb0be87 | ||
|
|
1c9c5232fe | ||
|
|
598d79a502 | ||
|
|
37d8360b77 | ||
|
|
82d9ca3317 | ||
|
|
4e4238d486 | ||
|
|
c77dbe44dc | ||
|
|
e03737f15f | ||
|
|
a02629bcd7 | ||
|
|
6c3fc23d78 | ||
|
|
0fe40f9ccb | ||
|
|
9bd7c8edd1 | ||
|
|
83ba480863 | ||
|
|
f158ea25e9 | ||
|
|
0227519eab | ||
|
|
616a9685fa | ||
|
|
fe61b01320 | ||
|
|
7b25144311 | ||
|
|
9d42fbbdd7 | ||
|
|
39ac5b088b | ||
|
|
c14ffd08a0 | ||
|
|
6e1239340b | ||
|
|
a297dc8b3b | ||
|
|
8d4ecc0898 | ||
|
|
eae9c04429 | ||
|
|
a41c48a9c5 | ||
|
|
ff2a94bd9b | ||
|
|
4a1f5558b8 | ||
|
|
608db9889f | ||
|
|
012b697337 | ||
|
|
0580506cf3 | ||
|
|
ff4ab9b661 | ||
|
|
b7ce5fdd3e | ||
|
|
a11e617322 | ||
|
|
d0beac7e2b | ||
|
|
9db497092f | ||
|
|
8eb91c08aa | ||
|
|
ded5437522 | ||
|
|
9348657951 | ||
|
|
bca85933f7 | ||
|
|
c32bb35f1c | ||
|
|
4b84062d62 | ||
|
|
d6d0f8fa17 | ||
|
|
dd72c875d3 | ||
|
|
1a1df50300 | ||
|
|
53cbb527b4 | ||
|
|
8b87b2717e | ||
|
|
1007d6dac7 | ||
|
|
6799fac120 | ||
|
|
558e6288ca | ||
|
|
d9cb73291b | ||
|
|
d0f7be3ac3 | ||
|
|
331e16d3ca | ||
|
|
0db246c311 | ||
|
|
94dc62ff58 | ||
|
|
e68ecf6844 | ||
|
|
5167b0a8c6 | ||
|
|
77e3d3786d | ||
|
|
708d4d39bc | ||
|
|
2a8cda2a1e | ||
|
|
8d783840ad | ||
|
|
abe39d5790 | ||
|
|
d7868e9e5a | ||
|
|
7b84e36e15 | ||
|
|
6cab6d69d8 | ||
|
|
87846d7aef | ||
|
|
2557769c6a | ||
|
|
48375f3878 | ||
|
|
176c85d8c1 | ||
|
|
17cad71ede | ||
|
|
e8bf9d4e6f | ||
|
|
7bdd2038ef | ||
|
|
e9f6e7943a | ||
|
|
e74ba387ab | ||
|
|
27c79e5b99 | ||
|
|
8170d5ea73 | ||
|
|
196f73705d | ||
|
|
ad0bbf5248 | ||
|
|
4cae9cd90d | ||
|
|
be7bc55a76 | ||
|
|
684b545e8f | ||
|
|
7835cc3b10 | ||
|
|
f8706b51e8 | ||
|
|
d97f8fd5da | ||
|
|
f8fa87441e | ||
|
|
d42537814a | ||
|
|
792421b0e2 | ||
|
|
72d55a010b | ||
|
|
880d8258ce | ||
|
|
b79bf82efb | ||
|
|
b3118b6253 | ||
|
|
ba172e2e25 | ||
|
|
892d53abeb | ||
|
|
5cbaa1ce98 | ||
|
|
7b35d9ad2e | ||
|
|
8462de7911 | ||
|
|
8721f44298 | ||
|
|
c7a2d69afa | ||
|
|
0453d81e7a | ||
|
|
501c04ac2b | ||
|
|
0ef4e9a5c3 | ||
|
|
129c50e598 | ||
|
|
3e276fc2ac | ||
|
|
658d5e05ae | ||
|
|
4e7d5d476e | ||
|
|
6a55ca20f3 | ||
|
|
c56c537f7f | ||
|
|
fd7d776121 | ||
|
|
1af28190d8 | ||
|
|
6b305be567 | ||
|
|
3bf70513b7 | ||
|
|
7e64404654 | ||
|
|
e1b5226f34 | ||
|
|
0d7128ad31 | ||
|
|
5778626087 | ||
|
|
3ff48756ed | ||
|
|
0ce9a6eeba | ||
|
|
ad527b4aed | ||
|
|
6633bb452e | ||
|
|
efeb0b4feb | ||
|
|
8cc11fc102 | ||
|
|
ee6a167220 | ||
|
|
8d4ad3c405 | ||
|
|
072fbf4d60 | ||
|
|
727c41c283 | ||
|
|
e2266838b6 | ||
|
|
775762d615 | ||
|
|
900c3008cb | ||
|
|
09379213a6 | ||
|
|
ceb97048e3 | ||
|
|
4561515517 | ||
|
|
a7b285759f | ||
|
|
b4531b2a12 | ||
|
|
9e1d261c76 | ||
|
|
e35fa15cd2 | ||
|
|
dbd1f0d4f9 | ||
|
|
9ade78b703 | ||
|
|
f20e244b5f | ||
|
|
0989308b7e | ||
|
|
12c7140536 | ||
|
|
2a0b605e92 | ||
|
|
6978890e6a | ||
|
|
561abd6cb9 | ||
|
|
4dd6227f0b | ||
|
|
1ec314c31c | ||
|
|
a2be5a00be | ||
|
|
4e2241c115 | ||
|
|
8459bca64a | ||
|
|
24cb0565b9 | ||
|
|
9442acb028 | ||
|
|
4f7f181a42 | ||
|
|
b7dd8737a7 | ||
|
|
2207eeb727 | ||
|
|
89dad7dfe7 | ||
|
|
e5803d0cf3 | ||
|
|
c1fffe9ae6 | ||
|
|
9e6cbd3d32 | ||
|
|
2ea8742510 | ||
|
|
5cfa0254f9 | ||
|
|
8cd2544f78 | ||
|
|
c03b768364 | ||
|
|
d60481ead4 | ||
|
|
126be3827d | ||
|
|
121274dca2 | ||
|
|
0ecf8da27e | ||
|
|
4a6bcb525d | ||
|
|
83f9ee50dd | ||
|
|
2bff297f79 | ||
|
|
dee68f6933 | ||
|
|
afa1e19c83 | ||
|
|
6052088eb4 | ||
|
|
c7fa5167c4 | ||
|
|
1034b0b146 | ||
|
|
8bcc4e5945 | ||
|
|
c3c24aa1db | ||
|
|
281c75d2d2 | ||
|
|
52307420f3 | ||
|
|
6185347cd8 | ||
|
|
b6cd29f77e | ||
|
|
b8ea8b1567 | ||
|
|
2f7dc98830 | ||
|
|
e248a99f79 | ||
|
|
4fb6d9aa5d | ||
|
|
f092ea8d67 | ||
|
|
c32cbbdda6 | ||
|
|
2497675259 | ||
|
|
8d084ab90a | ||
|
|
2398773ef0 | ||
|
|
a05998a30e | ||
|
|
f863c29194 | ||
|
|
d16a98c788 | ||
|
|
9421b02e96 | ||
|
|
10256864e4 | ||
|
|
85d010615d | ||
|
|
cd1cb186be | ||
|
|
4458354d70 | ||
|
|
0f27da8808 | ||
|
|
dd76bfa3c2 | ||
|
|
5780a66f7d | ||
|
|
d4342c034c | ||
|
|
1ec43f2530 | ||
|
|
3c300d8fdf | ||
|
|
23119b55d1 | ||
|
|
c8fb0e8f8a | ||
|
|
0ec32a77ef | ||
|
|
52921bfce8 | ||
|
|
960b929097 | ||
|
|
d4ce23eced | ||
|
|
6925510f44 | ||
|
|
9827ad4c22 |
@@ -1,7 +1,6 @@
|
|||||||
FROM python:3.9.2-slim
|
FROM python:3.9.2-slim
|
||||||
|
|
||||||
ENV TACTICAL_DIR /opt/tactical
|
ENV TACTICAL_DIR /opt/tactical
|
||||||
ENV TACTICAL_GO_DIR /usr/local/rmmgo
|
|
||||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||||
ENV WORKSPACE_DIR /workspace
|
ENV WORKSPACE_DIR /workspace
|
||||||
ENV TACTICAL_USER tactical
|
ENV TACTICAL_USER tactical
|
||||||
@@ -9,14 +8,11 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
|
|||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000 8383
|
||||||
|
|
||||||
RUN groupadd -g 1000 tactical && \
|
RUN groupadd -g 1000 tactical && \
|
||||||
useradd -u 1000 -g 1000 tactical
|
useradd -u 1000 -g 1000 tactical
|
||||||
|
|
||||||
# Copy Go Files
|
|
||||||
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
|
|
||||||
|
|
||||||
# Copy Dev python reqs
|
# Copy Dev python reqs
|
||||||
COPY ./requirements.txt /
|
COPY ./requirements.txt /
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ services:
|
|||||||
- tactical-backend
|
- tactical-backend
|
||||||
|
|
||||||
app-dev:
|
app-dev:
|
||||||
image: node:12-alpine
|
image: node:14-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
|
command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
|
||||||
working_dir: /workspace/web
|
working_dir: /workspace/web
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
@@ -175,6 +175,25 @@ services:
|
|||||||
- postgres-dev
|
- postgres-dev
|
||||||
- redis-dev
|
- redis-dev
|
||||||
|
|
||||||
|
# container for celery beat service
|
||||||
|
websockets-dev:
|
||||||
|
image: api-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./api.dockerfile
|
||||||
|
command: ["tactical-websockets-dev"]
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-websockets
|
||||||
|
volumes:
|
||||||
|
- tactical-data-dev:/opt/tactical
|
||||||
|
- ..:/workspace:cached
|
||||||
|
depends_on:
|
||||||
|
- postgres-dev
|
||||||
|
- redis-dev
|
||||||
|
|
||||||
nginx-dev:
|
nginx-dev:
|
||||||
# container for tactical reverse proxy
|
# container for tactical reverse proxy
|
||||||
image: ${IMAGE_REPO}tactical-nginx:${VERSION}
|
image: ${IMAGE_REPO}tactical-nginx:${VERSION}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ MESH_USERNAME = '${MESH_USER}'
|
|||||||
MESH_SITE = 'https://${MESH_HOST}'
|
MESH_SITE = 'https://${MESH_HOST}'
|
||||||
MESH_TOKEN_KEY = '${MESH_TOKEN}'
|
MESH_TOKEN_KEY = '${MESH_TOKEN}'
|
||||||
REDIS_HOST = '${REDIS_HOST}'
|
REDIS_HOST = '${REDIS_HOST}'
|
||||||
|
ADMIN_ENABLED = True
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
|
|||||||
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
|
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
|
||||||
|
|
||||||
# setup Python virtual env and install dependencies
|
# setup Python virtual env and install dependencies
|
||||||
! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
|
! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
|
||||||
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
|
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
|
||||||
|
|
||||||
django_setup
|
django_setup
|
||||||
@@ -149,9 +150,6 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" = 'tactical-api' ]; then
|
if [ "$1" = 'tactical-api' ]; then
|
||||||
cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
|
|
||||||
chmod +x /usr/local/bin/goversioninfo
|
|
||||||
|
|
||||||
check_tactical_ready
|
check_tactical_ready
|
||||||
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
|
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
|
||||||
fi
|
fi
|
||||||
@@ -166,3 +164,8 @@ if [ "$1" = 'tactical-celerybeat-dev' ]; then
|
|||||||
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
|
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
|
||||||
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
|
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'tactical-websockets-dev' ]; then
|
||||||
|
check_tactical_ready
|
||||||
|
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,40 +1,25 @@
|
|||||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
||||||
amqp==5.0.5
|
asyncio-nats-client
|
||||||
asgiref==3.3.1
|
celery
|
||||||
asyncio-nats-client==0.11.4
|
channels
|
||||||
billiard==3.6.3.0
|
Django
|
||||||
celery==5.0.5
|
django-cors-headers
|
||||||
certifi==2020.12.5
|
django-rest-knox
|
||||||
cffi==1.14.5
|
djangorestframework
|
||||||
chardet==4.0.0
|
loguru
|
||||||
cryptography==3.4.6
|
msgpack
|
||||||
decorator==4.4.2
|
psycopg2-binary
|
||||||
Django==3.1.7
|
pycparser
|
||||||
django-cors-headers==3.7.0
|
pycryptodome
|
||||||
django-rest-knox==4.1.0
|
pyotp
|
||||||
djangorestframework==3.12.2
|
pyparsing
|
||||||
future==0.18.2
|
pytz
|
||||||
kombu==5.0.2
|
qrcode
|
||||||
loguru==0.5.3
|
redis
|
||||||
msgpack==1.0.2
|
twilio
|
||||||
packaging==20.8
|
packaging
|
||||||
psycopg2-binary==2.8.6
|
validators
|
||||||
pycparser==2.20
|
websockets
|
||||||
pycryptodome==3.10.1
|
|
||||||
pyotp==2.6.0
|
|
||||||
pyparsing==2.4.7
|
|
||||||
pytz==2021.1
|
|
||||||
qrcode==6.1
|
|
||||||
redis==3.5.3
|
|
||||||
requests==2.25.1
|
|
||||||
six==1.15.0
|
|
||||||
sqlparse==0.4.1
|
|
||||||
twilio==6.52.0
|
|
||||||
urllib3==1.26.3
|
|
||||||
validators==0.18.2
|
|
||||||
vine==5.0.0
|
|
||||||
websockets==8.1
|
|
||||||
zipp==3.4.0
|
|
||||||
black
|
black
|
||||||
Werkzeug
|
Werkzeug
|
||||||
django-extensions
|
django-extensions
|
||||||
@@ -44,3 +29,7 @@ model_bakery
|
|||||||
mkdocs
|
mkdocs
|
||||||
mkdocs-material
|
mkdocs-material
|
||||||
pymdown-extensions
|
pymdown-extensions
|
||||||
|
Pygments
|
||||||
|
mypy
|
||||||
|
pysnooper
|
||||||
|
isort
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -3,7 +3,7 @@
|
|||||||
github: wh1te909
|
github: wh1te909
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: tacticalrmm
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
|||||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a bug report
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Server Info (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu 20.04, Debian 10]
|
||||||
|
- Browser: [e.g. chrome, safari]
|
||||||
|
- RMM Version (as shown in top left of web UI):
|
||||||
|
|
||||||
|
**Installation Method:**
|
||||||
|
- [ ] Standard
|
||||||
|
- [ ] Docker
|
||||||
|
|
||||||
|
**Agent Info (please complete the following information):**
|
||||||
|
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
|
||||||
|
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
22
.github/workflows/deploy-docs.yml
vendored
Normal file
22
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Deploy Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: docs
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
- run: pip install --upgrade pip
|
||||||
|
- run: pip install --upgrade setuptools wheel
|
||||||
|
- run: pip install mkdocs mkdocs-material pymdown-extensions
|
||||||
|
- run: mkdocs gh-deploy --force
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,3 +45,5 @@ htmlcov/
|
|||||||
docker-compose.dev.yml
|
docker-compose.dev.yml
|
||||||
docs/.vuepress/dist
|
docs/.vuepress/dist
|
||||||
nats-rmm.conf
|
nats-rmm.conf
|
||||||
|
.mypy_cache
|
||||||
|
docs/site/
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -3,7 +3,14 @@
|
|||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
"python.analysis.extraPaths": [
|
"python.analysis.extraPaths": [
|
||||||
"api/tacticalrmm",
|
"api/tacticalrmm",
|
||||||
|
"api/env",
|
||||||
],
|
],
|
||||||
|
"python.analysis.diagnosticSeverityOverrides": {
|
||||||
|
"reportUnusedImport": "error",
|
||||||
|
"reportDuplicateImport": "error",
|
||||||
|
},
|
||||||
|
"python.analysis.memory.keepLibraryAst": true,
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
"python.analysis.typeCheckingMode": "basic",
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
|||||||
100
README.md
100
README.md
@@ -8,13 +8,15 @@
|
|||||||
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
|
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
|
||||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
|
||||||
|
|
||||||
# [LIVE DEMO](https://rmm.xlawgaming.com/)
|
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
|
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
|
||||||
|
|
||||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
||||||
|
|
||||||
### [Discord Chat](https://discord.gg/upGTkWp)
|
### [Discord Chat](https://discord.gg/upGTkWp)
|
||||||
|
|
||||||
|
### [Documentation](https://wh1te909.github.io/tacticalrmm/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Teamviewer-like remote desktop control
|
- Teamviewer-like remote desktop control
|
||||||
@@ -33,98 +35,6 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
|
|||||||
|
|
||||||
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
|
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
|
||||||
|
|
||||||
## Installation
|
## Installation / Backup / Restore / Usage
|
||||||
|
|
||||||
### Requirements
|
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
|
||||||
- VPS with 2GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
|
|
||||||
- A domain you own with at least 3 subdomains
|
|
||||||
- Google Authenticator app (2 factor is NOT optional)
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
Refer to the [docker setup](docker/readme.md)
|
|
||||||
|
|
||||||
|
|
||||||
### Installation example (Ubuntu server 20.04 LTS)
|
|
||||||
|
|
||||||
Fresh VPS with latest updates\
|
|
||||||
login as root and create a user and add to sudoers group (we will be creating a user called tactical)
|
|
||||||
```
|
|
||||||
apt update && apt -y upgrade
|
|
||||||
adduser tactical
|
|
||||||
usermod -a -G sudo tactical
|
|
||||||
```
|
|
||||||
|
|
||||||
switch to the tactical user and setup the firewall
|
|
||||||
```
|
|
||||||
su - tactical
|
|
||||||
sudo ufw default deny incoming
|
|
||||||
sudo ufw default allow outgoing
|
|
||||||
sudo ufw allow ssh
|
|
||||||
sudo ufw allow http
|
|
||||||
sudo ufw allow https
|
|
||||||
sudo ufw allow proto tcp from any to any port 4222
|
|
||||||
sudo ufw enable && sudo ufw reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Our domain for this example is tacticalrmm.com
|
|
||||||
|
|
||||||
In the DNS manager of wherever our domain is hosted, we will create three A records, all pointing to the public IP address of our VPS
|
|
||||||
|
|
||||||
Create A record ```api.tacticalrmm.com``` for the django rest backend\
|
|
||||||
Create A record ```rmm.tacticalrmm.com``` for the vue frontend\
|
|
||||||
Create A record ```mesh.tacticalrmm.com``` for meshcentral
|
|
||||||
|
|
||||||
Download the install script and run it
|
|
||||||
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh
|
|
||||||
chmod +x install.sh
|
|
||||||
./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Links will be provided at the end of the install script.\
|
|
||||||
Download the executable from the first link, then open ```rmm.tacticalrmm.com``` and login.\
|
|
||||||
Upload the executable when prompted during the initial setup page.
|
|
||||||
|
|
||||||
|
|
||||||
### Install an agent
|
|
||||||
From the app's dashboard, choose Agents > Install Agent to generate an installer.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh)
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
|
|
||||||
chmod +x update.sh
|
|
||||||
./update.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup
|
|
||||||
Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh)
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
|
|
||||||
```
|
|
||||||
Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section)
|
|
||||||
|
|
||||||
Run it
|
|
||||||
```
|
|
||||||
chmod +x backup.sh
|
|
||||||
./backup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restore
|
|
||||||
Change your 3 A records to point to new server's public IP
|
|
||||||
|
|
||||||
Create same linux user account as old server and add to sudoers group and setup firewall (see install instructions above)
|
|
||||||
|
|
||||||
Copy backup file to new server
|
|
||||||
|
|
||||||
Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps.
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the restore script, passing it the backup tar file as the first argument
|
|
||||||
```
|
|
||||||
chmod +x restore.sh
|
|
||||||
./restore.sh rmm-backup-xxxxxxx.tar
|
|
||||||
```
|
|
||||||
@@ -7,7 +7,7 @@ from accounts.models import User
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Generates barcode for Google Authenticator and creates totp for user"
|
help = "Generates barcode for Authenticator and creates totp for user"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument("code", type=str)
|
parser.add_argument("code", type=str)
|
||||||
@@ -26,12 +26,10 @@ class Command(BaseCommand):
|
|||||||
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
||||||
subprocess.run(f'qr "{url}"', shell=True)
|
subprocess.run(f'qr "{url}"', shell=True)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS("Scan the barcode above with your authenticator app")
|
||||||
"Scan the barcode above with your google authenticator app"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
f"If that doesn't work you may manually enter the key: {code}"
|
f"If that doesn't work you may manually enter the setup key: {code}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
57
api/tacticalrmm/accounts/management/commands/reset_2fa.py
Normal file
57
api/tacticalrmm/accounts/management/commands/reset_2fa.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Reset 2fa"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("username", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
username = kwargs["username"]
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
domain = "Tactical RMM"
|
||||||
|
nginx = "/etc/nginx/sites-available/frontend.conf"
|
||||||
|
found = None
|
||||||
|
if os.path.exists(nginx):
|
||||||
|
try:
|
||||||
|
with open(nginx, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "server_name" in line:
|
||||||
|
found = line
|
||||||
|
break
|
||||||
|
|
||||||
|
if found:
|
||||||
|
rep = found.replace("server_name", "").replace(";", "")
|
||||||
|
domain = "".join(rep.split())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
code = pyotp.random_base32()
|
||||||
|
user.totp_key = code
|
||||||
|
user.save(update_fields=["totp_key"])
|
||||||
|
|
||||||
|
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
|
||||||
|
subprocess.run(f'qr "{url}"', shell=True)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING("Scan the barcode above with your authenticator app")
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f"If that doesn't work you may manually enter the setup key: {code}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"2fa was successfully reset for user {username}")
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Reset password for user"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("username", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
username = kwargs["username"]
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
passwd = input("Enter new password: ")
|
||||||
|
user.set_password(passwd)
|
||||||
|
user.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-02-28 06:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_user_default_agent_tbl_tab'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='agents_per_page',
|
||||||
|
field=models.PositiveIntegerField(default=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-09 02:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0012_user_agents_per_page'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='client_tree_sort',
|
||||||
|
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -15,6 +15,11 @@ AGENT_TBL_TAB_CHOICES = [
|
|||||||
("mixed", "Mixed"),
|
("mixed", "Mixed"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CLIENT_TREE_SORT_CHOICES = [
|
||||||
|
("alphafail", "Move failing clients to the top"),
|
||||||
|
("alpha", "Sort alphabetically"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser, BaseAuditModel):
|
class User(AbstractUser, BaseAuditModel):
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
@@ -27,6 +32,10 @@ class User(AbstractUser, BaseAuditModel):
|
|||||||
default_agent_tbl_tab = models.CharField(
|
default_agent_tbl_tab = models.CharField(
|
||||||
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||||
)
|
)
|
||||||
|
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
|
||||||
|
client_tree_sort = models.CharField(
|
||||||
|
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
|
||||||
|
)
|
||||||
|
|
||||||
agent = models.OneToOneField(
|
agent = models.OneToOneField(
|
||||||
"agents.Agent",
|
"agents.Agent",
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
|||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserUISerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"dark_mode",
|
||||||
|
"show_community_scripts",
|
||||||
|
"agent_dblclick_action",
|
||||||
|
"default_agent_tbl_tab",
|
||||||
|
"client_tree_sort",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|||||||
@@ -271,18 +271,13 @@ class TestUserAction(TacticalTestCase):
|
|||||||
|
|
||||||
def test_user_ui(self):
|
def test_user_ui(self):
|
||||||
url = "/accounts/users/ui/"
|
url = "/accounts/users/ui/"
|
||||||
data = {"dark_mode": False}
|
|
||||||
r = self.client.patch(url, data, format="json")
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
data = {"show_community_scripts": True}
|
|
||||||
r = self.client.patch(url, data, format="json")
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"userui": True,
|
"dark_mode": True,
|
||||||
|
"show_community_scripts": True,
|
||||||
"agent_dblclick_action": "editagent",
|
"agent_dblclick_action": "editagent",
|
||||||
"default_agent_tbl_tab": "mixed",
|
"default_agent_tbl_tab": "mixed",
|
||||||
|
"client_tree_sort": "alpha",
|
||||||
}
|
}
|
||||||
r = self.client.patch(url, data, format="json")
|
r = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|||||||
@@ -14,7 +14,15 @@ from logs.models import AuditLog
|
|||||||
from tacticalrmm.utils import notify_error
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .serializers import TOTPSetupSerializer, UserSerializer
|
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
|
||||||
|
|
||||||
|
|
||||||
|
def _is_root_user(request, user) -> bool:
|
||||||
|
return (
|
||||||
|
hasattr(settings, "ROOT_USER")
|
||||||
|
and request.user != user
|
||||||
|
and user.username == settings.ROOT_USER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CheckCreds(KnoxLoginView):
|
class CheckCreds(KnoxLoginView):
|
||||||
@@ -78,7 +86,7 @@ class GetAddUsers(APIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
# add new user
|
# add new user
|
||||||
try:
|
try:
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user( # type: ignore
|
||||||
request.data["username"],
|
request.data["username"],
|
||||||
request.data["email"],
|
request.data["email"],
|
||||||
request.data["password"],
|
request.data["password"],
|
||||||
@@ -105,11 +113,7 @@ class GetUpdateDeleteUser(APIView):
|
|||||||
def put(self, request, pk):
|
def put(self, request, pk):
|
||||||
user = get_object_or_404(User, pk=pk)
|
user = get_object_or_404(User, pk=pk)
|
||||||
|
|
||||||
if (
|
if _is_root_user(request, user):
|
||||||
hasattr(settings, "ROOT_USER")
|
|
||||||
and request.user != user
|
|
||||||
and user.username == settings.ROOT_USER
|
|
||||||
):
|
|
||||||
return notify_error("The root user cannot be modified from the UI")
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
serializer = UserSerializer(instance=user, data=request.data, partial=True)
|
||||||
@@ -120,11 +124,7 @@ class GetUpdateDeleteUser(APIView):
|
|||||||
|
|
||||||
def delete(self, request, pk):
|
def delete(self, request, pk):
|
||||||
user = get_object_or_404(User, pk=pk)
|
user = get_object_or_404(User, pk=pk)
|
||||||
if (
|
if _is_root_user(request, user):
|
||||||
hasattr(settings, "ROOT_USER")
|
|
||||||
and request.user != user
|
|
||||||
and user.username == settings.ROOT_USER
|
|
||||||
):
|
|
||||||
return notify_error("The root user cannot be deleted from the UI")
|
return notify_error("The root user cannot be deleted from the UI")
|
||||||
|
|
||||||
user.delete()
|
user.delete()
|
||||||
@@ -137,11 +137,7 @@ class UserActions(APIView):
|
|||||||
# reset password
|
# reset password
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
user = get_object_or_404(User, pk=request.data["id"])
|
user = get_object_or_404(User, pk=request.data["id"])
|
||||||
if (
|
if _is_root_user(request, user):
|
||||||
hasattr(settings, "ROOT_USER")
|
|
||||||
and request.user != user
|
|
||||||
and user.username == settings.ROOT_USER
|
|
||||||
):
|
|
||||||
return notify_error("The root user cannot be modified from the UI")
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
user.set_password(request.data["password"])
|
user.set_password(request.data["password"])
|
||||||
@@ -152,11 +148,7 @@ class UserActions(APIView):
|
|||||||
# reset two factor token
|
# reset two factor token
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
user = get_object_or_404(User, pk=request.data["id"])
|
user = get_object_or_404(User, pk=request.data["id"])
|
||||||
if (
|
if _is_root_user(request, user):
|
||||||
hasattr(settings, "ROOT_USER")
|
|
||||||
and request.user != user
|
|
||||||
and user.username == settings.ROOT_USER
|
|
||||||
):
|
|
||||||
return notify_error("The root user cannot be modified from the UI")
|
return notify_error("The root user cannot be modified from the UI")
|
||||||
|
|
||||||
user.totp_key = ""
|
user.totp_key = ""
|
||||||
@@ -184,19 +176,9 @@ class TOTPSetup(APIView):
|
|||||||
|
|
||||||
class UserUI(APIView):
|
class UserUI(APIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = request.user
|
serializer = UserUISerializer(
|
||||||
|
instance=request.user, data=request.data, partial=True
|
||||||
if "dark_mode" in request.data.keys():
|
)
|
||||||
user.dark_mode = request.data["dark_mode"]
|
serializer.is_valid(raise_exception=True)
|
||||||
user.save(update_fields=["dark_mode"])
|
serializer.save()
|
||||||
|
|
||||||
if "show_community_scripts" in request.data.keys():
|
|
||||||
user.show_community_scripts = request.data["show_community_scripts"]
|
|
||||||
user.save(update_fields=["show_community_scripts"])
|
|
||||||
|
|
||||||
if "userui" in request.data.keys():
|
|
||||||
user.agent_dblclick_action = request.data["agent_dblclick_action"]
|
|
||||||
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
|
|
||||||
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
|
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Agent, Note, RecoveryAction
|
from .models import Agent, AgentCustomField, Note, RecoveryAction
|
||||||
|
|
||||||
admin.site.register(Agent)
|
admin.site.register(Agent)
|
||||||
admin.site.register(RecoveryAction)
|
admin.site.register(RecoveryAction)
|
||||||
admin.site.register(Note)
|
admin.site.register(Note)
|
||||||
|
admin.site.register(AgentCustomField)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from itertools import cycle
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
from model_bakery.recipe import Recipe, foreign_key
|
from model_bakery.recipe import Recipe, foreign_key, seq
|
||||||
|
|
||||||
|
|
||||||
def generate_agent_id(hostname):
|
def generate_agent_id(hostname):
|
||||||
@@ -30,8 +30,7 @@ agent = Recipe(
|
|||||||
hostname="DESKTOP-TEST123",
|
hostname="DESKTOP-TEST123",
|
||||||
version="1.3.0",
|
version="1.3.0",
|
||||||
monitoring_type=cycle(["workstation", "server"]),
|
monitoring_type=cycle(["workstation", "server"]),
|
||||||
salt_id=generate_agent_id("DESKTOP-TEST123"),
|
agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
|
||||||
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
server_agent = agent.extend(
|
server_agent = agent.extend(
|
||||||
@@ -44,6 +43,10 @@ workstation_agent = agent.extend(
|
|||||||
|
|
||||||
online_agent = agent.extend(last_seen=djangotime.now())
|
online_agent = agent.extend(last_seen=djangotime.now())
|
||||||
|
|
||||||
|
offline_agent = agent.extend(
|
||||||
|
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
|
||||||
|
)
|
||||||
|
|
||||||
overdue_agent = agent.extend(
|
overdue_agent = agent.extend(
|
||||||
last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
|
last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
from clients.models import Client, Site
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Bulk update agent offline/overdue time"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("time", type=int, help="Time in minutes")
|
||||||
|
parser.add_argument(
|
||||||
|
"--client",
|
||||||
|
type=str,
|
||||||
|
help="Client Name",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--site",
|
||||||
|
type=str,
|
||||||
|
help="Site Name",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--offline",
|
||||||
|
action="store_true",
|
||||||
|
help="Offline",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--overdue",
|
||||||
|
action="store_true",
|
||||||
|
help="Overdue",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="All agents",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
time = kwargs["time"]
|
||||||
|
client_name = kwargs["client"]
|
||||||
|
site_name = kwargs["site"]
|
||||||
|
all_agents = kwargs["all"]
|
||||||
|
offline = kwargs["offline"]
|
||||||
|
overdue = kwargs["overdue"]
|
||||||
|
agents = None
|
||||||
|
|
||||||
|
if offline and time < 2:
|
||||||
|
self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if overdue and time < 3:
|
||||||
|
self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if client_name:
|
||||||
|
try:
|
||||||
|
client = Client.objects.get(name=client_name)
|
||||||
|
except Client.DoesNotExist:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f"Client {client_name} doesn't exist")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
agents = Agent.objects.filter(site__client=client)
|
||||||
|
|
||||||
|
elif site_name:
|
||||||
|
try:
|
||||||
|
site = Site.objects.get(name=site_name)
|
||||||
|
except Site.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
agents = Agent.objects.filter(site=site)
|
||||||
|
|
||||||
|
elif all_agents:
|
||||||
|
agents = Agent.objects.all()
|
||||||
|
|
||||||
|
if agents:
|
||||||
|
if offline:
|
||||||
|
agents.update(offline_time=time)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Changed offline time on {len(agents)} agents to {time} minutes"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if overdue:
|
||||||
|
agents.update(overdue_time=time)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Changed overdue time on {len(agents)} agents to {time} minutes"
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Shows online agents that are not on the latest version"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
|
||||||
|
"pk", "version", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
agents = [i for i in q if i.status == "online"]
|
||||||
|
for agent in agents:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-04 03:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0006_auto_20210217_1736'),
|
||||||
|
('agents', '0030_agent_offline_time'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agent',
|
||||||
|
name='alert_template',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_customfield'),
|
||||||
|
('agents', '0031_agent_alert_template'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgentCustomField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('value', models.TextField(blank=True, null=True)),
|
||||||
|
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
|
||||||
|
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 02:51
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0032_agentcustomfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
name='multiple_value',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 03:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0033_agentcustomfield_multiple_value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
name='checkbox_value',
|
||||||
|
field=models.BooleanField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0034_agentcustomfield_checkbox_value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
old_name='checkbox_value',
|
||||||
|
new_name='bool_value',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='agentcustomfield',
|
||||||
|
old_name='value',
|
||||||
|
new_name='string_value',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from typing import Any, List, Union
|
from typing import Any
|
||||||
|
|
||||||
import msgpack
|
import msgpack
|
||||||
import validators
|
import validators
|
||||||
@@ -13,14 +13,13 @@ from Crypto.Hash import SHA3_384
|
|||||||
from Crypto.Random import get_random_bytes
|
from Crypto.Random import get_random_bytes
|
||||||
from Crypto.Util.Padding import pad
|
from Crypto.Util.Padding import pad
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from nats.aio.client import Client as NATS
|
from nats.aio.client import Client as NATS
|
||||||
from nats.aio.errors import ErrTimeout
|
from nats.aio.errors import ErrTimeout
|
||||||
from packaging import version as pyver
|
|
||||||
|
|
||||||
from alerts.models import AlertTemplate
|
|
||||||
from core.models import TZ_CHOICES, CoreSettings
|
from core.models import TZ_CHOICES, CoreSettings
|
||||||
from logs.models import BaseAuditModel
|
from logs.models import BaseAuditModel
|
||||||
|
|
||||||
@@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
|
|||||||
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
||||||
)
|
)
|
||||||
maintenance_mode = models.BooleanField(default=False)
|
maintenance_mode = models.BooleanField(default=False)
|
||||||
|
alert_template = models.ForeignKey(
|
||||||
|
"alerts.AlertTemplate",
|
||||||
|
related_name="agents",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
site = models.ForeignKey(
|
site = models.ForeignKey(
|
||||||
"clients.Site",
|
"clients.Site",
|
||||||
related_name="agents",
|
related_name="agents",
|
||||||
@@ -85,7 +91,7 @@ class Agent(BaseAuditModel):
|
|||||||
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
|
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||||
|
|
||||||
# check if new agent has been create
|
# check if new agent has been created
|
||||||
# or check if policy have changed on agent
|
# or check if policy have changed on agent
|
||||||
# or if site has changed on agent and if so generate-policies
|
# or if site has changed on agent and if so generate-policies
|
||||||
if (
|
if (
|
||||||
@@ -104,14 +110,6 @@ class Agent(BaseAuditModel):
|
|||||||
def client(self):
|
def client(self):
|
||||||
return self.site.client
|
return self.site.client
|
||||||
|
|
||||||
@property
|
|
||||||
def has_nats(self):
|
|
||||||
return pyver.parse(self.version) >= pyver.parse("1.1.0")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_gotasks(self):
|
|
||||||
return pyver.parse(self.version) >= pyver.parse("1.1.1")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self):
|
||||||
# return the default timezone unless the timezone is explicity set per agent
|
# return the default timezone unless the timezone is explicity set per agent
|
||||||
@@ -164,14 +162,14 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_patches_pending(self):
|
def has_patches_pending(self):
|
||||||
return self.winupdates.filter(action="approve").filter(installed=False).exists()
|
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def checks(self):
|
def checks(self):
|
||||||
total, passing, failing = 0, 0, 0
|
total, passing, failing = 0, 0, 0
|
||||||
|
|
||||||
if self.agentchecks.exists():
|
if self.agentchecks.exists(): # type: ignore
|
||||||
for i in self.agentchecks.all():
|
for i in self.agentchecks.all(): # type: ignore
|
||||||
total += 1
|
total += 1
|
||||||
if i.status == "passing":
|
if i.status == "passing":
|
||||||
passing += 1
|
passing += 1
|
||||||
@@ -241,6 +239,7 @@ class Agent(BaseAuditModel):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
|
||||||
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
|
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -270,10 +269,24 @@ class Agent(BaseAuditModel):
|
|||||||
except:
|
except:
|
||||||
return ["unknown disk"]
|
return ["unknown disk"]
|
||||||
|
|
||||||
|
def check_run_interval(self) -> int:
|
||||||
|
interval = self.check_interval
|
||||||
|
# determine if any agent checks have a custom interval and set the lowest interval
|
||||||
|
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
|
||||||
|
if check.run_interval and check.run_interval < interval:
|
||||||
|
|
||||||
|
# don't allow check runs less than 15s
|
||||||
|
if check.run_interval < 15:
|
||||||
|
interval = 15
|
||||||
|
else:
|
||||||
|
interval = check.run_interval
|
||||||
|
|
||||||
|
return interval
|
||||||
|
|
||||||
def run_script(
|
def run_script(
|
||||||
self,
|
self,
|
||||||
scriptpk: int,
|
scriptpk: int,
|
||||||
args: List[str] = [],
|
args: list[str] = [],
|
||||||
timeout: int = 120,
|
timeout: int = 120,
|
||||||
full: bool = False,
|
full: bool = False,
|
||||||
wait: bool = False,
|
wait: bool = False,
|
||||||
@@ -283,10 +296,13 @@ class Agent(BaseAuditModel):
|
|||||||
from scripts.models import Script
|
from scripts.models import Script
|
||||||
|
|
||||||
script = Script.objects.get(pk=scriptpk)
|
script = Script.objects.get(pk=scriptpk)
|
||||||
|
|
||||||
|
parsed_args = script.parse_script_args(self, script.shell, args)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"func": "runscriptfull" if full else "runscript",
|
"func": "runscriptfull" if full else "runscript",
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
"script_args": args,
|
"script_args": parsed_args,
|
||||||
"payload": {
|
"payload": {
|
||||||
"code": script.code,
|
"code": script.code,
|
||||||
"shell": script.shell,
|
"shell": script.shell,
|
||||||
@@ -295,10 +311,10 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
running_agent = self
|
running_agent = self
|
||||||
if run_on_any:
|
if run_on_any:
|
||||||
nats_ping = {"func": "ping", "timeout": 1}
|
nats_ping = {"func": "ping"}
|
||||||
|
|
||||||
# try on self first
|
# try on self first
|
||||||
r = asyncio.run(self.nats_cmd(nats_ping))
|
r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
|
||||||
|
|
||||||
if r == "pong":
|
if r == "pong":
|
||||||
running_agent = self
|
running_agent = self
|
||||||
@@ -312,7 +328,7 @@ class Agent(BaseAuditModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for agent in online:
|
for agent in online:
|
||||||
r = asyncio.run(agent.nats_cmd(nats_ping))
|
r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
|
||||||
if r == "pong":
|
if r == "pong":
|
||||||
running_agent = agent
|
running_agent = agent
|
||||||
break
|
break
|
||||||
@@ -333,27 +349,27 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
updates = list()
|
updates = list()
|
||||||
if patch_policy.critical == "approve":
|
if patch_policy.critical == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Critical", installed=False
|
severity="Critical", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.important == "approve":
|
if patch_policy.important == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Important", installed=False
|
severity="Important", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.moderate == "approve":
|
if patch_policy.moderate == "approve":
|
||||||
updates += self.winupdates.filter(
|
updates += self.winupdates.filter( # type: ignore
|
||||||
severity="Moderate", installed=False
|
severity="Moderate", installed=False
|
||||||
).exclude(action="approve")
|
).exclude(action="approve")
|
||||||
|
|
||||||
if patch_policy.low == "approve":
|
if patch_policy.low == "approve":
|
||||||
updates += self.winupdates.filter(severity="Low", installed=False).exclude(
|
updates += self.winupdates.filter(severity="Low", installed=False).exclude( # type: ignore
|
||||||
action="approve"
|
action="approve"
|
||||||
)
|
)
|
||||||
|
|
||||||
if patch_policy.other == "approve":
|
if patch_policy.other == "approve":
|
||||||
updates += self.winupdates.filter(severity="", installed=False).exclude(
|
updates += self.winupdates.filter(severity="", installed=False).exclude( # type: ignore
|
||||||
action="approve"
|
action="approve"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -368,7 +384,7 @@ class Agent(BaseAuditModel):
|
|||||||
site = self.site
|
site = self.site
|
||||||
core_settings = CoreSettings.objects.first()
|
core_settings = CoreSettings.objects.first()
|
||||||
patch_policy = None
|
patch_policy = None
|
||||||
agent_policy = self.winupdatepolicy.get()
|
agent_policy = self.winupdatepolicy.get() # type: ignore
|
||||||
|
|
||||||
if self.monitoring_type == "server":
|
if self.monitoring_type == "server":
|
||||||
# check agent policy first which should override client or site policy
|
# check agent policy first which should override client or site policy
|
||||||
@@ -453,16 +469,16 @@ class Agent(BaseAuditModel):
|
|||||||
|
|
||||||
return patch_policy
|
return patch_policy
|
||||||
|
|
||||||
def get_approved_update_guids(self) -> List[str]:
|
def get_approved_update_guids(self) -> list[str]:
|
||||||
return list(
|
return list(
|
||||||
self.winupdates.filter(action="approve", installed=False).values_list(
|
self.winupdates.filter(action="approve", installed=False).values_list( # type: ignore
|
||||||
"guid", flat=True
|
"guid", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# returns alert template assigned in the following order: policy, site, client, global
|
# sets alert template assigned in the following order: policy, site, client, global
|
||||||
# will return None if nothing is found
|
# sets None if nothing is found
|
||||||
def get_alert_template(self) -> Union[AlertTemplate, None]:
|
def set_alert_template(self):
|
||||||
|
|
||||||
site = self.site
|
site = self.site
|
||||||
client = self.client
|
client = self.client
|
||||||
@@ -562,16 +578,23 @@ class Agent(BaseAuditModel):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# save alert_template to agent cache field
|
||||||
|
self.alert_template = template
|
||||||
|
self.save()
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|
||||||
# no alert templates found or agent has been excluded
|
# no alert templates found or agent has been excluded
|
||||||
|
self.alert_template = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def generate_checks_from_policies(self):
|
def generate_checks_from_policies(self):
|
||||||
from automation.models import Policy
|
from automation.models import Policy
|
||||||
|
|
||||||
# Clear agent checks that have overriden_by_policy set
|
# Clear agent checks that have overriden_by_policy set
|
||||||
self.agentchecks.update(overriden_by_policy=False)
|
self.agentchecks.update(overriden_by_policy=False) # type: ignore
|
||||||
|
|
||||||
# Generate checks based on policies
|
# Generate checks based on policies
|
||||||
Policy.generate_policy_checks(self)
|
Policy.generate_policy_checks(self)
|
||||||
@@ -606,7 +629,7 @@ class Agent(BaseAuditModel):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return "err"
|
return "err"
|
||||||
|
|
||||||
async def nats_cmd(self, data, timeout=30, wait=True):
|
async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True):
|
||||||
nc = NATS()
|
nc = NATS()
|
||||||
options = {
|
options = {
|
||||||
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||||
@@ -628,7 +651,7 @@ class Agent(BaseAuditModel):
|
|||||||
except ErrTimeout:
|
except ErrTimeout:
|
||||||
ret = "timeout"
|
ret = "timeout"
|
||||||
else:
|
else:
|
||||||
ret = msgpack.loads(msg.data)
|
ret = msgpack.loads(msg.data) # type: ignore
|
||||||
|
|
||||||
await nc.close()
|
await nc.close()
|
||||||
return ret
|
return ret
|
||||||
@@ -650,12 +673,12 @@ class Agent(BaseAuditModel):
|
|||||||
def delete_superseded_updates(self):
|
def delete_superseded_updates(self):
|
||||||
try:
|
try:
|
||||||
pks = [] # list of pks to delete
|
pks = [] # list of pks to delete
|
||||||
kbs = list(self.winupdates.values_list("kb", flat=True))
|
kbs = list(self.winupdates.values_list("kb", flat=True)) # type: ignore
|
||||||
d = Counter(kbs)
|
d = Counter(kbs)
|
||||||
dupes = [k for k, v in d.items() if v > 1]
|
dupes = [k for k, v in d.items() if v > 1]
|
||||||
|
|
||||||
for dupe in dupes:
|
for dupe in dupes:
|
||||||
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
|
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True) # type: ignore
|
||||||
# extract the version from the title and sort from oldest to newest
|
# extract the version from the title and sort from oldest to newest
|
||||||
# skip if no version info is available therefore nothing to parse
|
# skip if no version info is available therefore nothing to parse
|
||||||
try:
|
try:
|
||||||
@@ -668,17 +691,17 @@ class Agent(BaseAuditModel):
|
|||||||
continue
|
continue
|
||||||
# append all but the latest version to our list of pks to delete
|
# append all but the latest version to our list of pks to delete
|
||||||
for ver in sorted_vers[:-1]:
|
for ver in sorted_vers[:-1]:
|
||||||
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
|
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver) # type: ignore
|
||||||
pks.append(q.first().pk)
|
pks.append(q.first().pk)
|
||||||
|
|
||||||
pks = list(set(pks))
|
pks = list(set(pks))
|
||||||
self.winupdates.filter(pk__in=pks).delete()
|
self.winupdates.filter(pk__in=pks).delete() # type: ignore
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# define how the agent should handle pending actions
|
# define how the agent should handle pending actions
|
||||||
def handle_pending_actions(self):
|
def handle_pending_actions(self):
|
||||||
pending_actions = self.pendingactions.filter(status="pending")
|
pending_actions = self.pendingactions.filter(status="pending") # type: ignore
|
||||||
|
|
||||||
for action in pending_actions:
|
for action in pending_actions:
|
||||||
if action.action_type == "taskaction":
|
if action.action_type == "taskaction":
|
||||||
@@ -702,89 +725,12 @@ class Agent(BaseAuditModel):
|
|||||||
# for clearing duplicate pending actions on agent
|
# for clearing duplicate pending actions on agent
|
||||||
def remove_matching_pending_task_actions(self, task_id):
|
def remove_matching_pending_task_actions(self, task_id):
|
||||||
# remove any other pending actions on agent with same task_id
|
# remove any other pending actions on agent with same task_id
|
||||||
for action in self.pendingactions.exclude(status="completed"):
|
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
|
||||||
if action.details["task_id"] == task_id:
|
if action.details["task_id"] == task_id:
|
||||||
action.delete()
|
action.delete()
|
||||||
|
|
||||||
def handle_alert(self, checkin: bool = False) -> None:
|
def should_create_alert(self, alert_template=None):
|
||||||
from agents.tasks import (
|
return (
|
||||||
agent_outage_email_task,
|
|
||||||
agent_outage_sms_task,
|
|
||||||
agent_recovery_email_task,
|
|
||||||
agent_recovery_sms_task,
|
|
||||||
)
|
|
||||||
from alerts.models import Alert
|
|
||||||
|
|
||||||
# return if agent is in maintenace mode
|
|
||||||
if self.maintenance_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
alert_template = self.get_alert_template()
|
|
||||||
|
|
||||||
# called when agent is back online
|
|
||||||
if checkin:
|
|
||||||
if Alert.objects.filter(agent=self, resolved=False).exists():
|
|
||||||
|
|
||||||
# resolve alert if exists
|
|
||||||
alert = Alert.objects.get(agent=self, resolved=False)
|
|
||||||
alert.resolve()
|
|
||||||
|
|
||||||
# check if a resolved notification should be emailed
|
|
||||||
if (
|
|
||||||
not alert.resolved_email_sent
|
|
||||||
and alert_template
|
|
||||||
and alert_template.agent_email_on_resolved
|
|
||||||
or self.overdue_email_alert
|
|
||||||
):
|
|
||||||
agent_recovery_email_task.delay(pk=alert.pk)
|
|
||||||
|
|
||||||
# check if a resolved notification should be texted
|
|
||||||
if (
|
|
||||||
not alert.resolved_sms_sent
|
|
||||||
and alert_template
|
|
||||||
and alert_template.agent_text_on_resolved
|
|
||||||
or self.overdue_text_alert
|
|
||||||
):
|
|
||||||
agent_recovery_sms_task.delay(pk=alert.pk)
|
|
||||||
|
|
||||||
# check if any scripts should be run
|
|
||||||
if (
|
|
||||||
not alert.resolved_action_run
|
|
||||||
and alert_template
|
|
||||||
and alert_template.resolved_action
|
|
||||||
):
|
|
||||||
r = self.run_script(
|
|
||||||
scriptpk=alert_template.resolved_action.pk,
|
|
||||||
args=alert_template.resolved_action_args,
|
|
||||||
timeout=alert_template.resolved_action_timeout,
|
|
||||||
wait=True,
|
|
||||||
full=True,
|
|
||||||
run_on_any=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# command was successful
|
|
||||||
if type(r) == dict:
|
|
||||||
alert.resolved_action_retcode = r["retcode"]
|
|
||||||
alert.resolved_action_stdout = r["stdout"]
|
|
||||||
alert.resolved_action_stderr = r["stderr"]
|
|
||||||
alert.resolved_action_execution_time = "{:.4f}".format(
|
|
||||||
r["execution_time"]
|
|
||||||
)
|
|
||||||
alert.resolved_action_run = djangotime.now()
|
|
||||||
alert.save()
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
|
|
||||||
)
|
|
||||||
|
|
||||||
# called when agent is offline
|
|
||||||
else:
|
|
||||||
|
|
||||||
# check if alert hasn't been created yet so create it
|
|
||||||
if not Alert.objects.filter(agent=self, resolved=False).exists():
|
|
||||||
|
|
||||||
# check if alert should be created and if not return
|
|
||||||
if (
|
|
||||||
self.overdue_dashboard_alert
|
self.overdue_dashboard_alert
|
||||||
or self.overdue_email_alert
|
or self.overdue_email_alert
|
||||||
or self.overdue_text_alert
|
or self.overdue_text_alert
|
||||||
@@ -796,83 +742,12 @@ class Agent(BaseAuditModel):
|
|||||||
or alert_template.agent_always_text
|
or alert_template.agent_always_text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
|
||||||
alert = Alert.create_availability_alert(self)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 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):
|
def send_outage_email(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.get_alert_template()
|
|
||||||
CORE.send_mail(
|
CORE.send_mail(
|
||||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||||
(
|
(
|
||||||
@@ -881,14 +756,13 @@ class Agent(BaseAuditModel):
|
|||||||
f"agent {self.hostname} "
|
f"agent {self.hostname} "
|
||||||
"within the expected time."
|
"within the expected time."
|
||||||
),
|
),
|
||||||
alert_template=alert_template,
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_recovery_email(self):
|
def send_recovery_email(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.get_alert_template()
|
|
||||||
CORE.send_mail(
|
CORE.send_mail(
|
||||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||||
(
|
(
|
||||||
@@ -897,27 +771,25 @@ class Agent(BaseAuditModel):
|
|||||||
f"agent {self.hostname} "
|
f"agent {self.hostname} "
|
||||||
"after an interruption in data transmission."
|
"after an interruption in data transmission."
|
||||||
),
|
),
|
||||||
alert_template=alert_template,
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_outage_sms(self):
|
def send_outage_sms(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
alert_template = self.get_alert_template()
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
CORE.send_sms(
|
CORE.send_sms(
|
||||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||||
alert_template=alert_template,
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_recovery_sms(self):
|
def send_recovery_sms(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.get_alert_template()
|
|
||||||
CORE.send_sms(
|
CORE.send_sms(
|
||||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||||
alert_template=alert_template,
|
alert_template=self.alert_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -968,3 +840,38 @@ class Note(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.agent.hostname
|
return self.agent.hostname
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCustomField(models.Model):
|
||||||
|
agent = models.ForeignKey(
|
||||||
|
Agent,
|
||||||
|
related_name="custom_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
field = models.ForeignKey(
|
||||||
|
"core.CustomField",
|
||||||
|
related_name="agent_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
string_value = models.TextField(null=True, blank=True)
|
||||||
|
bool_value = models.BooleanField(blank=True, default=False)
|
||||||
|
multiple_value = ArrayField(
|
||||||
|
models.TextField(null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.field
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self.field.type == "multiple":
|
||||||
|
return self.multiple_value
|
||||||
|
elif self.field.type == "checkbox":
|
||||||
|
return self.bool_value
|
||||||
|
else:
|
||||||
|
return self.string_value
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from rest_framework import serializers
|
|||||||
from clients.serializers import ClientSerializer
|
from clients.serializers import ClientSerializer
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
from .models import Agent, Note
|
from .models import Agent, AgentCustomField, Note
|
||||||
|
|
||||||
|
|
||||||
class AgentSerializer(serializers.ModelSerializer):
|
class AgentSerializer(serializers.ModelSerializer):
|
||||||
@@ -57,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
|||||||
alert_template = serializers.SerializerMethodField()
|
alert_template = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_alert_template(self, obj):
|
def get_alert_template(self, obj):
|
||||||
alert_template = obj.get_alert_template()
|
|
||||||
|
|
||||||
if not alert_template:
|
if not obj.alert_template:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"name": alert_template.name,
|
"name": obj.alert_template.name,
|
||||||
"always_email": alert_template.agent_always_email,
|
"always_email": obj.alert_template.agent_always_email,
|
||||||
"always_text": alert_template.agent_always_text,
|
"always_text": obj.alert_template.agent_always_text,
|
||||||
"always_alert": alert_template.agent_always_alert,
|
"always_alert": obj.alert_template.agent_always_alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_pending_actions(self, obj):
|
def get_pending_actions(self, obj):
|
||||||
@@ -120,10 +119,30 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
|||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCustomFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = AgentCustomField
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"field",
|
||||||
|
"agent",
|
||||||
|
"value",
|
||||||
|
"string_value",
|
||||||
|
"bool_value",
|
||||||
|
"multiple_value",
|
||||||
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
"string_value": {"write_only": True},
|
||||||
|
"bool_value": {"write_only": True},
|
||||||
|
"multiple_value": {"write_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AgentEditSerializer(serializers.ModelSerializer):
|
class AgentEditSerializer(serializers.ModelSerializer):
|
||||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||||
all_timezones = serializers.SerializerMethodField()
|
all_timezones = serializers.SerializerMethodField()
|
||||||
client = ClientSerializer(read_only=True)
|
client = ClientSerializer(read_only=True)
|
||||||
|
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
def get_all_timezones(self, obj):
|
def get_all_timezones(self, obj):
|
||||||
return pytz.all_timezones
|
return pytz.all_timezones
|
||||||
@@ -147,6 +166,7 @@ class AgentEditSerializer(serializers.ModelSerializer):
|
|||||||
"all_timezones",
|
"all_timezones",
|
||||||
"winupdatepolicy",
|
"winupdatepolicy",
|
||||||
"policy",
|
"policy",
|
||||||
|
"custom_fields",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import List, Union
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
@@ -77,7 +80,7 @@ def agent_update(pk: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def send_agent_update_task(pks: List[int]) -> None:
|
def send_agent_update_task(pks: list[int]) -> None:
|
||||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
for pk in chunk:
|
for pk in chunk:
|
||||||
@@ -93,7 +96,7 @@ def auto_self_agent_update_task() -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
q = Agent.objects.only("pk", "version")
|
q = Agent.objects.only("pk", "version")
|
||||||
pks: List[int] = [
|
pks: list[int] = [
|
||||||
i.pk
|
i.pk
|
||||||
for i in q
|
for i in q
|
||||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
@@ -183,6 +186,8 @@ def agent_recovery_sms_task(pk: int) -> str:
|
|||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def agent_outages_task() -> None:
|
def agent_outages_task() -> None:
|
||||||
|
from alerts.models import Alert
|
||||||
|
|
||||||
agents = Agent.objects.only(
|
agents = Agent.objects.only(
|
||||||
"pk",
|
"pk",
|
||||||
"last_seen",
|
"last_seen",
|
||||||
@@ -195,21 +200,7 @@ def agent_outages_task() -> None:
|
|||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
if agent.status == "overdue":
|
if agent.status == "overdue":
|
||||||
agent.handle_alert()
|
Alert.handle_alert_failure(agent)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def handle_agent_recovery_task(pk: int) -> None:
|
|
||||||
sleep(10)
|
|
||||||
from agents.models import RecoveryAction
|
|
||||||
|
|
||||||
action = RecoveryAction.objects.get(pk=pk)
|
|
||||||
if action.mode == "command":
|
|
||||||
data = {"func": "recoverycmd", "recoverycommand": action.command}
|
|
||||||
else:
|
|
||||||
data = {"func": "recover", "payload": {"mode": action.mode}}
|
|
||||||
|
|
||||||
asyncio.run(action.agent.nats_cmd(data, wait=False))
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
@@ -217,8 +208,8 @@ def run_script_email_results_task(
|
|||||||
agentpk: int,
|
agentpk: int,
|
||||||
scriptpk: int,
|
scriptpk: int,
|
||||||
nats_timeout: int,
|
nats_timeout: int,
|
||||||
emails: List[str],
|
emails: list[str],
|
||||||
args: List[str] = [],
|
args: list[str] = [],
|
||||||
):
|
):
|
||||||
agent = Agent.objects.get(pk=agentpk)
|
agent = Agent.objects.get(pk=agentpk)
|
||||||
script = Script.objects.get(pk=scriptpk)
|
script = Script.objects.get(pk=scriptpk)
|
||||||
@@ -264,3 +255,48 @@ def run_script_email_results_task(
|
|||||||
server.quit()
|
server.quit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nats_config() -> dict:
|
||||||
|
return {
|
||||||
|
"key": settings.SECRET_KEY,
|
||||||
|
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def monitor_agents_task() -> None:
|
||||||
|
agents = Agent.objects.only(
|
||||||
|
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
ret = [i.agent_id for i in agents if i.status != "online"]
|
||||||
|
config = _get_nats_config()
|
||||||
|
config["agents"] = ret
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
with open(fp.name, "w") as f:
|
||||||
|
json.dump(config, f)
|
||||||
|
|
||||||
|
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "monitor"]
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, timeout=30)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def get_wmi_task() -> None:
|
||||||
|
agents = Agent.objects.only(
|
||||||
|
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||||
|
)
|
||||||
|
ret = [i.agent_id for i in agents if i.status == "online"]
|
||||||
|
config = _get_nats_config()
|
||||||
|
config["agents"] = ret
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
with open(fp.name, "w") as f:
|
||||||
|
json.dump(config, f)
|
||||||
|
|
||||||
|
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "wmi"]
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, timeout=30)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from typing import List
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -13,11 +12,68 @@ from tacticalrmm.test import TacticalTestCase
|
|||||||
from winupdate.models import WinUpdatePolicy
|
from winupdate.models import WinUpdatePolicy
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
from .models import Agent
|
from .models import Agent, AgentCustomField
|
||||||
from .serializers import AgentSerializer
|
from .serializers import AgentSerializer
|
||||||
from .tasks import auto_self_agent_update_task
|
from .tasks import auto_self_agent_update_task
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgentsList(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.authenticate()
|
||||||
|
self.setup_coresettings()
|
||||||
|
|
||||||
|
def test_agents_list(self):
|
||||||
|
url = "/agents/listagents/"
|
||||||
|
|
||||||
|
# 36 total agents
|
||||||
|
company1 = baker.make("clients.Client")
|
||||||
|
company2 = baker.make("clients.Client")
|
||||||
|
site1 = baker.make("clients.Site", client=company1)
|
||||||
|
site2 = baker.make("clients.Site", client=company1)
|
||||||
|
site3 = baker.make("clients.Site", client=company2)
|
||||||
|
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.online_agent", site=site1, monitoring_type="server", _quantity=15
|
||||||
|
)
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.online_agent",
|
||||||
|
site=site2,
|
||||||
|
monitoring_type="workstation",
|
||||||
|
_quantity=10,
|
||||||
|
)
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.online_agent",
|
||||||
|
site=site3,
|
||||||
|
monitoring_type="server",
|
||||||
|
_quantity=4,
|
||||||
|
)
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.online_agent",
|
||||||
|
site=site3,
|
||||||
|
monitoring_type="workstation",
|
||||||
|
_quantity=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test all agents
|
||||||
|
r = self.client.patch(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data), 36) # type: ignore
|
||||||
|
|
||||||
|
# test client1
|
||||||
|
data = {"clientPK": company1.pk} # type: ignore
|
||||||
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data), 25) # type: ignore
|
||||||
|
|
||||||
|
# test site3
|
||||||
|
data = {"sitePK": site3.pk} # type: ignore
|
||||||
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data), 11) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
|
|
||||||
class TestAgentViews(TacticalTestCase):
|
class TestAgentViews(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
@@ -78,12 +134,12 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
_quantity=15,
|
_quantity=15,
|
||||||
)
|
)
|
||||||
|
|
||||||
pks: List[int] = list(
|
pks: list[int] = list(
|
||||||
Agent.objects.only("pk", "version").values_list("pk", flat=True)
|
Agent.objects.only("pk", "version").values_list("pk", flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {"pks": pks}
|
data = {"pks": pks}
|
||||||
expected: List[int] = [
|
expected: list[int] = [
|
||||||
i.pk
|
i.pk
|
||||||
for i in Agent.objects.only("pk", "version")
|
for i in Agent.objects.only("pk", "version")
|
||||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
@@ -142,11 +198,6 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
|
|
||||||
@patch("agents.models.Agent.nats_cmd")
|
@patch("agents.models.Agent.nats_cmd")
|
||||||
def test_get_processes(self, mock_ret):
|
def test_get_processes(self, mock_ret):
|
||||||
agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
|
|
||||||
url_old = f"/agents/{agent_old.pk}/getprocs/"
|
|
||||||
r = self.client.get(url_old)
|
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
|
|
||||||
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
|
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
|
||||||
url = f"/agents/{agent.pk}/getprocs/"
|
url = f"/agents/{agent.pk}/getprocs/"
|
||||||
|
|
||||||
@@ -257,7 +308,7 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
mock_ret.return_value = "nt authority\system"
|
mock_ret.return_value = "nt authority\system"
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertIsInstance(r.data, str)
|
self.assertIsInstance(r.data, str) # type: ignore
|
||||||
|
|
||||||
mock_ret.return_value = "timeout"
|
mock_ret.return_value = "timeout"
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
@@ -277,15 +328,16 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
nats_cmd.return_value = "ok"
|
nats_cmd.return_value = "ok"
|
||||||
r = self.client.patch(url, data, format="json")
|
r = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
|
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") # type: ignore
|
||||||
self.assertEqual(r.data["agent"], self.agent.hostname)
|
self.assertEqual(r.data["agent"], self.agent.hostname) # type: ignore
|
||||||
|
|
||||||
nats_data = {
|
nats_data = {
|
||||||
"func": "schedtask",
|
"func": "schedtask",
|
||||||
"schedtaskpayload": {
|
"schedtaskpayload": {
|
||||||
"type": "schedreboot",
|
"type": "schedreboot",
|
||||||
|
"deleteafter": True,
|
||||||
"trigger": "once",
|
"trigger": "once",
|
||||||
"name": r.data["task_name"],
|
"name": r.data["task_name"], # type: ignore
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"month": "August",
|
"month": "August",
|
||||||
"day": 29,
|
"day": 29,
|
||||||
@@ -306,53 +358,43 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
r = self.client.patch(url, data_invalid, format="json")
|
r = self.client.patch(url, data_invalid, format="json")
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
self.assertEqual(r.data, "Invalid date")
|
self.assertEqual(r.data, "Invalid date") # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
@patch("os.path.exists")
|
@patch("os.path.exists")
|
||||||
@patch("subprocess.run")
|
def test_install_agent(self, mock_file_exists):
|
||||||
def test_install_agent(self, mock_subprocess, mock_file_exists):
|
url = "/agents/installagent/"
|
||||||
url = f"/agents/installagent/"
|
|
||||||
|
|
||||||
site = baker.make("clients.Site")
|
site = baker.make("clients.Site")
|
||||||
data = {
|
data = {
|
||||||
"client": site.client.id,
|
"client": site.client.id, # type: ignore
|
||||||
"site": site.id,
|
"site": site.id, # type: ignore
|
||||||
"arch": "64",
|
"arch": "64",
|
||||||
"expires": 23,
|
"expires": 23,
|
||||||
"installMethod": "exe",
|
"installMethod": "manual",
|
||||||
"api": "https://api.example.com",
|
"api": "https://api.example.com",
|
||||||
"agenttype": "server",
|
"agenttype": "server",
|
||||||
"rdp": 1,
|
"rdp": 1,
|
||||||
"ping": 0,
|
"ping": 0,
|
||||||
"power": 0,
|
"power": 0,
|
||||||
|
"fileName": "rmm-client-site-server.exe",
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_file_exists.return_value = False
|
mock_file_exists.return_value = False
|
||||||
mock_subprocess.return_value.returncode = 0
|
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 406)
|
self.assertEqual(r.status_code, 406)
|
||||||
|
|
||||||
mock_file_exists.return_value = True
|
mock_file_exists.return_value = True
|
||||||
mock_subprocess.return_value.returncode = 1
|
|
||||||
r = self.client.post(url, data, format="json")
|
|
||||||
self.assertEqual(r.status_code, 413)
|
|
||||||
|
|
||||||
mock_file_exists.return_value = True
|
|
||||||
mock_subprocess.return_value.returncode = 0
|
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
data["arch"] = "32"
|
data["arch"] = "32"
|
||||||
mock_subprocess.return_value.returncode = 0
|
|
||||||
mock_file_exists.return_value = False
|
mock_file_exists.return_value = False
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 415)
|
self.assertEqual(r.status_code, 415)
|
||||||
|
|
||||||
data["installMethod"] = "manual"
|
|
||||||
data["arch"] = "64"
|
data["arch"] = "64"
|
||||||
mock_subprocess.return_value.returncode = 0
|
|
||||||
mock_file_exists.return_value = True
|
mock_file_exists.return_value = True
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertIn("rdp", r.json()["cmd"])
|
self.assertIn("rdp", r.json()["cmd"])
|
||||||
@@ -363,52 +405,74 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
self.assertIn("power", r.json()["cmd"])
|
self.assertIn("power", r.json()["cmd"])
|
||||||
self.assertIn("ping", r.json()["cmd"])
|
self.assertIn("ping", r.json()["cmd"])
|
||||||
|
|
||||||
|
data["installMethod"] = "powershell"
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
def test_recover(self):
|
@patch("agents.models.Agent.nats_cmd")
|
||||||
|
def test_recover(self, nats_cmd):
|
||||||
from agents.models import RecoveryAction
|
from agents.models import RecoveryAction
|
||||||
|
|
||||||
self.agent.version = "0.11.1"
|
RecoveryAction.objects.all().delete()
|
||||||
self.agent.save(update_fields=["version"])
|
|
||||||
url = "/agents/recover/"
|
url = "/agents/recover/"
|
||||||
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
|
|
||||||
|
# test mesh realtime
|
||||||
|
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
|
||||||
|
nats_cmd.return_value = "ok"
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||||
|
nats_cmd.assert_called_with(
|
||||||
|
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
|
||||||
|
)
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
data["mode"] = "mesh"
|
# test mesh with agent rpc not working
|
||||||
r = self.client.post(url, data, format="json")
|
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
|
||||||
self.assertEqual(r.status_code, 400)
|
nats_cmd.return_value = "timeout"
|
||||||
self.assertIn("pending", r.json())
|
|
||||||
|
|
||||||
RecoveryAction.objects.all().delete()
|
|
||||||
data["mode"] = "command"
|
|
||||||
data["cmd"] = "ipconfig /flushdns"
|
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 1)
|
||||||
RecoveryAction.objects.all().delete()
|
mesh_recovery = RecoveryAction.objects.first()
|
||||||
data["cmd"] = None
|
self.assertEqual(mesh_recovery.mode, "mesh")
|
||||||
r = self.client.post(url, data, format="json")
|
nats_cmd.reset_mock()
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
|
|
||||||
RecoveryAction.objects.all().delete()
|
RecoveryAction.objects.all().delete()
|
||||||
|
|
||||||
self.agent.version = "0.9.4"
|
# test tacagent realtime
|
||||||
self.agent.save(update_fields=["version"])
|
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
|
||||||
data["mode"] = "mesh"
|
nats_cmd.return_value = "ok"
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
self.assertIn("0.9.5", r.json())
|
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
|
||||||
|
|
||||||
def test_agents_list(self):
|
|
||||||
url = "/agents/listagents/"
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||||
|
nats_cmd.assert_called_with(
|
||||||
|
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
|
||||||
|
)
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
# test tacagent with rpc not working
|
||||||
|
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
|
||||||
|
nats_cmd.return_value = "timeout"
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
|
# test shell cmd without command
|
||||||
|
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 0)
|
||||||
|
|
||||||
|
# test shell cmd
|
||||||
|
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(RecoveryAction.objects.count(), 1)
|
||||||
|
cmd_recovery = RecoveryAction.objects.first()
|
||||||
|
self.assertEqual(cmd_recovery.mode, "command")
|
||||||
|
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
|
||||||
|
|
||||||
def test_agents_agent_detail(self):
|
def test_agents_agent_detail(self):
|
||||||
url = f"/agents/{self.agent.pk}/agentdetail/"
|
url = f"/agents/{self.agent.pk}/agentdetail/"
|
||||||
@@ -426,7 +490,7 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
|
|
||||||
edit = {
|
edit = {
|
||||||
"id": self.agent.pk,
|
"id": self.agent.pk,
|
||||||
"site": site.id,
|
"site": site.id, # type: ignore
|
||||||
"monitoring_type": "workstation",
|
"monitoring_type": "workstation",
|
||||||
"description": "asjdk234andasd",
|
"description": "asjdk234andasd",
|
||||||
"offline_time": 4,
|
"offline_time": 4,
|
||||||
@@ -457,12 +521,41 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
|
|
||||||
agent = Agent.objects.get(pk=self.agent.pk)
|
agent = Agent.objects.get(pk=self.agent.pk)
|
||||||
data = AgentSerializer(agent).data
|
data = AgentSerializer(agent).data
|
||||||
self.assertEqual(data["site"], site.id)
|
self.assertEqual(data["site"], site.id) # type: ignore
|
||||||
|
|
||||||
policy = WinUpdatePolicy.objects.get(agent=self.agent)
|
policy = WinUpdatePolicy.objects.get(agent=self.agent)
|
||||||
data = WinUpdatePolicySerializer(policy).data
|
data = WinUpdatePolicySerializer(policy).data
|
||||||
self.assertEqual(data["run_time_days"], [2, 3, 6])
|
self.assertEqual(data["run_time_days"], [2, 3, 6])
|
||||||
|
|
||||||
|
# test adding custom fields
|
||||||
|
field = baker.make("core.CustomField", model="agent", type="number")
|
||||||
|
edit = {
|
||||||
|
"id": self.agent.pk,
|
||||||
|
"site": site.id, # type: ignore
|
||||||
|
"description": "asjdk234andasd",
|
||||||
|
"custom_fields": [{"field": field.id, "string_value": "123"}], # type: ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, edit, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
AgentCustomField.objects.filter(agent=self.agent, field=field).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# test edit custom field
|
||||||
|
edit = {
|
||||||
|
"id": self.agent.pk,
|
||||||
|
"site": site.id, # type: ignore
|
||||||
|
"description": "asjdk234andasd",
|
||||||
|
"custom_fields": [{"field": field.id, "string_value": "456"}], # type: ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.patch(url, edit, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
AgentCustomField.objects.get(agent=agent, field=field).value,
|
||||||
|
"456",
|
||||||
|
)
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
@patch("agents.models.Agent.get_login_token")
|
@patch("agents.models.Agent.get_login_token")
|
||||||
@@ -475,21 +568,21 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
# TODO
|
# TODO
|
||||||
# decode the cookie
|
# decode the cookie
|
||||||
|
|
||||||
self.assertIn("&viewmode=13", r.data["file"])
|
self.assertIn("&viewmode=13", r.data["file"]) # type: ignore
|
||||||
self.assertIn("&viewmode=12", r.data["terminal"])
|
self.assertIn("&viewmode=12", r.data["terminal"]) # type: ignore
|
||||||
self.assertIn("&viewmode=11", r.data["control"])
|
self.assertIn("&viewmode=11", r.data["control"]) # type: ignore
|
||||||
|
|
||||||
self.assertIn("&gotonode=", r.data["file"])
|
self.assertIn("&gotonode=", r.data["file"]) # type: ignore
|
||||||
self.assertIn("&gotonode=", r.data["terminal"])
|
self.assertIn("&gotonode=", r.data["terminal"]) # type: ignore
|
||||||
self.assertIn("&gotonode=", r.data["control"])
|
self.assertIn("&gotonode=", r.data["control"]) # type: ignore
|
||||||
|
|
||||||
self.assertIn("?login=", r.data["file"])
|
self.assertIn("?login=", r.data["file"]) # type: ignore
|
||||||
self.assertIn("?login=", r.data["terminal"])
|
self.assertIn("?login=", r.data["terminal"]) # type: ignore
|
||||||
self.assertIn("?login=", r.data["control"])
|
self.assertIn("?login=", r.data["control"]) # type: ignore
|
||||||
|
|
||||||
self.assertEqual(self.agent.hostname, r.data["hostname"])
|
self.assertEqual(self.agent.hostname, r.data["hostname"]) # type: ignore
|
||||||
self.assertEqual(self.agent.client.name, r.data["client"])
|
self.assertEqual(self.agent.client.name, r.data["client"]) # type: ignore
|
||||||
self.assertEqual(self.agent.site.name, r.data["site"])
|
self.assertEqual(self.agent.site.name, r.data["site"]) # type: ignore
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
@@ -499,32 +592,6 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_by_client(self):
|
|
||||||
url = f"/agents/byclient/{self.agent.client.id}/"
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertTrue(r.data)
|
|
||||||
|
|
||||||
url = f"/agents/byclient/500/"
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertFalse(r.data) # returns empty list
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
|
|
||||||
def test_by_site(self):
|
|
||||||
url = f"/agents/bysite/{self.agent.site.id}/"
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertTrue(r.data)
|
|
||||||
|
|
||||||
url = f"/agents/bysite/500/"
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.data, [])
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
|
|
||||||
def test_overdue_action(self):
|
def test_overdue_action(self):
|
||||||
url = "/agents/overdueaction/"
|
url = "/agents/overdueaction/"
|
||||||
|
|
||||||
@@ -533,14 +600,14 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
agent = Agent.objects.get(pk=self.agent.pk)
|
agent = Agent.objects.get(pk=self.agent.pk)
|
||||||
self.assertTrue(agent.overdue_email_alert)
|
self.assertTrue(agent.overdue_email_alert)
|
||||||
self.assertEqual(self.agent.hostname, r.data)
|
self.assertEqual(self.agent.hostname, r.data) # type: ignore
|
||||||
|
|
||||||
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
|
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
|
||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
agent = Agent.objects.get(pk=self.agent.pk)
|
agent = Agent.objects.get(pk=self.agent.pk)
|
||||||
self.assertFalse(agent.overdue_text_alert)
|
self.assertFalse(agent.overdue_text_alert)
|
||||||
self.assertEqual(self.agent.hostname, r.data)
|
self.assertEqual(self.agent.hostname, r.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
@@ -684,7 +751,7 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
nats_cmd.return_value = "ok"
|
nats_cmd.return_value = "ok"
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertIn(self.agent.hostname, r.data)
|
self.assertIn(self.agent.hostname, r.data) # type: ignore
|
||||||
nats_cmd.assert_called_with(
|
nats_cmd.assert_called_with(
|
||||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
|
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
|
||||||
)
|
)
|
||||||
@@ -699,13 +766,84 @@ class TestAgentViews(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
@patch("agents.tasks.run_script_email_results_task.delay")
|
||||||
|
@patch("agents.models.Agent.run_script")
|
||||||
|
def test_run_script(self, run_script, email_task):
|
||||||
|
run_script.return_value = "ok"
|
||||||
|
url = "/agents/runscript/"
|
||||||
|
script = baker.make_recipe("scripts.script")
|
||||||
|
|
||||||
|
# test wait
|
||||||
|
data = {
|
||||||
|
"pk": self.agent.pk,
|
||||||
|
"scriptPK": script.pk,
|
||||||
|
"output": "wait",
|
||||||
|
"args": [],
|
||||||
|
"timeout": 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
run_script.assert_called_with(
|
||||||
|
scriptpk=script.pk, args=[], timeout=18, wait=True
|
||||||
|
)
|
||||||
|
run_script.reset_mock()
|
||||||
|
|
||||||
|
# test email default
|
||||||
|
data = {
|
||||||
|
"pk": self.agent.pk,
|
||||||
|
"scriptPK": script.pk,
|
||||||
|
"output": "email",
|
||||||
|
"args": ["abc", "123"],
|
||||||
|
"timeout": 15,
|
||||||
|
"emailmode": "default",
|
||||||
|
"emails": ["admin@example.com", "bob@example.com"],
|
||||||
|
}
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
email_task.assert_called_with(
|
||||||
|
agentpk=self.agent.pk,
|
||||||
|
scriptpk=script.pk,
|
||||||
|
nats_timeout=18,
|
||||||
|
emails=[],
|
||||||
|
args=["abc", "123"],
|
||||||
|
)
|
||||||
|
email_task.reset_mock()
|
||||||
|
|
||||||
|
# test email overrides
|
||||||
|
data["emailmode"] = "custom"
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
email_task.assert_called_with(
|
||||||
|
agentpk=self.agent.pk,
|
||||||
|
scriptpk=script.pk,
|
||||||
|
nats_timeout=18,
|
||||||
|
emails=["admin@example.com", "bob@example.com"],
|
||||||
|
args=["abc", "123"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# test fire and forget
|
||||||
|
data = {
|
||||||
|
"pk": self.agent.pk,
|
||||||
|
"scriptPK": script.pk,
|
||||||
|
"output": "forget",
|
||||||
|
"args": ["hello", "world"],
|
||||||
|
"timeout": 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
run_script.assert_called_with(
|
||||||
|
scriptpk=script.pk, args=["hello", "world"], timeout=25
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAgentViewsNew(TacticalTestCase):
|
class TestAgentViewsNew(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
self.setup_coresettings()
|
self.setup_coresettings()
|
||||||
|
|
||||||
def test_agent_counts(self):
|
""" def test_agent_counts(self):
|
||||||
url = "/agents/agent_counts/"
|
url = "/agents/agent_counts/"
|
||||||
|
|
||||||
# create some data
|
# create some data
|
||||||
@@ -730,9 +868,9 @@ class TestAgentViewsNew(TacticalTestCase):
|
|||||||
|
|
||||||
r = self.client.post(url, format="json")
|
r = self.client.post(url, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, data)
|
self.assertEqual(r.data, data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url) """
|
||||||
|
|
||||||
def test_agent_maintenance_mode(self):
|
def test_agent_maintenance_mode(self):
|
||||||
url = "/agents/maintenance/"
|
url = "/agents/maintenance/"
|
||||||
@@ -742,14 +880,14 @@ class TestAgentViewsNew(TacticalTestCase):
|
|||||||
agent = baker.make_recipe("agents.agent", site=site)
|
agent = baker.make_recipe("agents.agent", site=site)
|
||||||
|
|
||||||
# Test client toggle maintenance mode
|
# Test client toggle maintenance mode
|
||||||
data = {"type": "Client", "id": site.client.id, "action": True}
|
data = {"type": "Client", "id": site.client.id, "action": True} # type: ignore
|
||||||
|
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
|
self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
|
||||||
|
|
||||||
# Test site toggle maintenance mode
|
# Test site toggle maintenance mode
|
||||||
data = {"type": "Site", "id": site.id, "action": False}
|
data = {"type": "Site", "id": site.id, "action": False} # type: ignore
|
||||||
|
|
||||||
r = self.client.post(url, data, format="json")
|
r = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ urlpatterns = [
|
|||||||
path("listagents/", views.AgentsTableList.as_view()),
|
path("listagents/", views.AgentsTableList.as_view()),
|
||||||
path("listagentsnodetail/", views.list_agents_no_detail),
|
path("listagentsnodetail/", views.list_agents_no_detail),
|
||||||
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
|
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
|
||||||
path("byclient/<int:clientpk>/", views.by_client),
|
|
||||||
path("bysite/<int:sitepk>/", views.by_site),
|
|
||||||
path("overdueaction/", views.overdue_action),
|
path("overdueaction/", views.overdue_action),
|
||||||
path("sendrawcmd/", views.send_raw_cmd),
|
path("sendrawcmd/", views.send_raw_cmd),
|
||||||
path("<pk>/agentdetail/", views.agent_detail),
|
path("<pk>/agentdetail/", views.agent_detail),
|
||||||
@@ -29,7 +27,6 @@ urlpatterns = [
|
|||||||
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
|
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
|
||||||
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
|
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
|
||||||
path("bulk/", views.bulk),
|
path("bulk/", views.bulk),
|
||||||
path("agent_counts/", views.agent_counts),
|
|
||||||
path("maintenance/", views.agent_maintenance),
|
path("maintenance/", views.agent_maintenance),
|
||||||
path("<int:pk>/wmi/", views.WMI.as_view()),
|
path("<int:pk>/wmi/", views.WMI.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import datetime as dt
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import subprocess
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from packaging import version as pyver
|
from packaging import version as pyver
|
||||||
from rest_framework import generics, status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -24,8 +22,9 @@ from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
|
|||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||||
|
|
||||||
from .models import Agent, Note, RecoveryAction
|
from .models import Agent, AgentCustomField, Note, RecoveryAction
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
|
AgentCustomFieldSerializer,
|
||||||
AgentEditSerializer,
|
AgentEditSerializer,
|
||||||
AgentHostnameSerializer,
|
AgentHostnameSerializer,
|
||||||
AgentOverdueActionSerializer,
|
AgentOverdueActionSerializer,
|
||||||
@@ -53,7 +52,7 @@ def get_agent_versions(request):
|
|||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def update_agents(request):
|
def update_agents(request):
|
||||||
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
||||||
pks: List[int] = [
|
pks: list[int] = [
|
||||||
i.pk
|
i.pk
|
||||||
for i in q
|
for i in q
|
||||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||||
@@ -66,7 +65,6 @@ def update_agents(request):
|
|||||||
def ping(request, pk):
|
def ping(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
status = "offline"
|
status = "offline"
|
||||||
if agent.has_nats:
|
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
||||||
if r == "pong":
|
if r == "pong":
|
||||||
status = "online"
|
status = "online"
|
||||||
@@ -77,7 +75,6 @@ def ping(request, pk):
|
|||||||
@api_view(["DELETE"])
|
@api_view(["DELETE"])
|
||||||
def uninstall(request):
|
def uninstall(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if agent.has_nats:
|
|
||||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||||
|
|
||||||
name = agent.hostname
|
name = agent.hostname
|
||||||
@@ -86,7 +83,7 @@ def uninstall(request):
|
|||||||
return Response(f"{name} will now be uninstalled.")
|
return Response(f"{name} will now be uninstalled.")
|
||||||
|
|
||||||
|
|
||||||
@api_view(["PATCH"])
|
@api_view(["PATCH", "PUT"])
|
||||||
def edit_agent(request):
|
def edit_agent(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||||
|
|
||||||
@@ -95,13 +92,36 @@ def edit_agent(request):
|
|||||||
a_serializer.save()
|
a_serializer.save()
|
||||||
|
|
||||||
if "winupdatepolicy" in request.data.keys():
|
if "winupdatepolicy" in request.data.keys():
|
||||||
policy = agent.winupdatepolicy.get()
|
policy = agent.winupdatepolicy.get() # type: ignore
|
||||||
p_serializer = WinUpdatePolicySerializer(
|
p_serializer = WinUpdatePolicySerializer(
|
||||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||||
)
|
)
|
||||||
p_serializer.is_valid(raise_exception=True)
|
p_serializer.is_valid(raise_exception=True)
|
||||||
p_serializer.save()
|
p_serializer.save()
|
||||||
|
|
||||||
|
if "custom_fields" in request.data.keys():
|
||||||
|
|
||||||
|
for field in request.data["custom_fields"]:
|
||||||
|
|
||||||
|
custom_field = field
|
||||||
|
custom_field["agent"] = agent.id # type: ignore
|
||||||
|
|
||||||
|
if AgentCustomField.objects.filter(
|
||||||
|
field=field["field"], agent=agent.id # type: ignore
|
||||||
|
):
|
||||||
|
value = AgentCustomField.objects.get(
|
||||||
|
field=field["field"], agent=agent.id # type: ignore
|
||||||
|
)
|
||||||
|
serializer = AgentCustomFieldSerializer(
|
||||||
|
instance=value, data=custom_field
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
else:
|
||||||
|
serializer = AgentCustomFieldSerializer(data=custom_field)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
@@ -144,9 +164,6 @@ def agent_detail(request, pk):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def get_processes(request, pk):
|
def get_processes(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if pyver.parse(agent.version) < pyver.parse("1.2.0"):
|
|
||||||
return notify_error("Requires agent version 1.2.0 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||||
if r == "timeout":
|
if r == "timeout":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
@@ -156,9 +173,6 @@ def get_processes(request, pk):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def kill_proc(request, pk, pid):
|
def kill_proc(request, pk, pid):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(
|
r = asyncio.run(
|
||||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||||
)
|
)
|
||||||
@@ -174,8 +188,6 @@ def kill_proc(request, pk, pid):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def get_event_log(request, pk, logtype, days):
|
def get_event_log(request, pk, logtype, days):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
timeout = 180 if logtype == "Security" else 30
|
timeout = 180 if logtype == "Security" else 30
|
||||||
data = {
|
data = {
|
||||||
"func": "eventlog",
|
"func": "eventlog",
|
||||||
@@ -195,8 +207,6 @@ def get_event_log(request, pk, logtype, days):
|
|||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def send_raw_cmd(request):
|
def send_raw_cmd(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
timeout = int(request.data["timeout"])
|
timeout = int(request.data["timeout"])
|
||||||
data = {
|
data = {
|
||||||
"func": "rawcmd",
|
"func": "rawcmd",
|
||||||
@@ -221,15 +231,32 @@ def send_raw_cmd(request):
|
|||||||
return Response(r)
|
return Response(r)
|
||||||
|
|
||||||
|
|
||||||
class AgentsTableList(generics.ListAPIView):
|
class AgentsTableList(APIView):
|
||||||
|
def patch(self, request):
|
||||||
|
if "sitePK" in request.data.keys():
|
||||||
queryset = (
|
queryset = (
|
||||||
Agent.objects.select_related("site")
|
Agent.objects.select_related("site", "policy", "alert_template")
|
||||||
.prefetch_related("agentchecks")
|
.prefetch_related("agentchecks")
|
||||||
.only(
|
.filter(site_id=request.data["sitePK"])
|
||||||
|
)
|
||||||
|
elif "clientPK" in request.data.keys():
|
||||||
|
queryset = (
|
||||||
|
Agent.objects.select_related("site", "policy", "alert_template")
|
||||||
|
.prefetch_related("agentchecks")
|
||||||
|
.filter(site__client_id=request.data["clientPK"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = Agent.objects.select_related(
|
||||||
|
"site", "policy", "alert_template"
|
||||||
|
).prefetch_related("agentchecks")
|
||||||
|
|
||||||
|
queryset = queryset.only(
|
||||||
"pk",
|
"pk",
|
||||||
"hostname",
|
"hostname",
|
||||||
"agent_id",
|
"agent_id",
|
||||||
"site",
|
"site",
|
||||||
|
"policy",
|
||||||
|
"alert_template",
|
||||||
"monitoring_type",
|
"monitoring_type",
|
||||||
"description",
|
"description",
|
||||||
"needs_reboot",
|
"needs_reboot",
|
||||||
@@ -244,11 +271,6 @@ class AgentsTableList(generics.ListAPIView):
|
|||||||
"time_zone",
|
"time_zone",
|
||||||
"maintenance_mode",
|
"maintenance_mode",
|
||||||
)
|
)
|
||||||
)
|
|
||||||
serializer_class = AgentTableSerializer
|
|
||||||
|
|
||||||
def list(self, request):
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
ctx = {"default_tz": get_default_timezone()}
|
ctx = {"default_tz": get_default_timezone()}
|
||||||
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
|
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
@@ -266,66 +288,6 @@ def agent_edit_details(request, pk):
|
|||||||
return Response(AgentEditSerializer(agent).data)
|
return Response(AgentEditSerializer(agent).data)
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
|
||||||
def by_client(request, clientpk):
|
|
||||||
agents = (
|
|
||||||
Agent.objects.select_related("site")
|
|
||||||
.filter(site__client_id=clientpk)
|
|
||||||
.prefetch_related("agentchecks")
|
|
||||||
.only(
|
|
||||||
"pk",
|
|
||||||
"hostname",
|
|
||||||
"agent_id",
|
|
||||||
"site",
|
|
||||||
"monitoring_type",
|
|
||||||
"description",
|
|
||||||
"needs_reboot",
|
|
||||||
"overdue_text_alert",
|
|
||||||
"overdue_email_alert",
|
|
||||||
"overdue_time",
|
|
||||||
"offline_time",
|
|
||||||
"last_seen",
|
|
||||||
"boot_time",
|
|
||||||
"logged_in_username",
|
|
||||||
"last_logged_in_user",
|
|
||||||
"time_zone",
|
|
||||||
"maintenance_mode",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ctx = {"default_tz": get_default_timezone()}
|
|
||||||
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
|
||||||
def by_site(request, sitepk):
|
|
||||||
agents = (
|
|
||||||
Agent.objects.filter(site_id=sitepk)
|
|
||||||
.select_related("site")
|
|
||||||
.prefetch_related("agentchecks")
|
|
||||||
.only(
|
|
||||||
"pk",
|
|
||||||
"hostname",
|
|
||||||
"agent_id",
|
|
||||||
"site",
|
|
||||||
"monitoring_type",
|
|
||||||
"description",
|
|
||||||
"needs_reboot",
|
|
||||||
"overdue_text_alert",
|
|
||||||
"overdue_email_alert",
|
|
||||||
"overdue_time",
|
|
||||||
"offline_time",
|
|
||||||
"last_seen",
|
|
||||||
"boot_time",
|
|
||||||
"logged_in_username",
|
|
||||||
"last_logged_in_user",
|
|
||||||
"time_zone",
|
|
||||||
"maintenance_mode",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ctx = {"default_tz": get_default_timezone()}
|
|
||||||
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def overdue_action(request):
|
def overdue_action(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
@@ -341,9 +303,6 @@ class Reboot(APIView):
|
|||||||
# reboot now
|
# reboot now
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
@@ -353,8 +312,6 @@ class Reboot(APIView):
|
|||||||
# reboot later
|
# reboot later
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_gotasks:
|
|
||||||
return notify_error("Requires agent version 1.1.1 or greater")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
||||||
@@ -369,6 +326,7 @@ class Reboot(APIView):
|
|||||||
"func": "schedtask",
|
"func": "schedtask",
|
||||||
"schedtaskpayload": {
|
"schedtaskpayload": {
|
||||||
"type": "schedreboot",
|
"type": "schedreboot",
|
||||||
|
"deleteafter": True,
|
||||||
"trigger": "once",
|
"trigger": "once",
|
||||||
"name": task_name,
|
"name": task_name,
|
||||||
"year": int(dt.datetime.strftime(obj, "%Y")),
|
"year": int(dt.datetime.strftime(obj, "%Y")),
|
||||||
@@ -379,9 +337,6 @@ class Reboot(APIView):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if pyver.parse(agent.version) >= pyver.parse("1.1.2"):
|
|
||||||
nats_data["schedtaskpayload"]["deleteafter"] = True
|
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
|
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
return notify_error(r)
|
return notify_error(r)
|
||||||
@@ -427,124 +382,20 @@ def install_agent(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if request.data["installMethod"] == "exe":
|
if request.data["installMethod"] == "exe":
|
||||||
go_bin = "/usr/local/rmmgo/go/bin/go"
|
from tacticalrmm.utils import generate_winagent_exe
|
||||||
|
|
||||||
if not os.path.exists(go_bin):
|
return generate_winagent_exe(
|
||||||
return Response("nogolang", status=status.HTTP_409_CONFLICT)
|
client=client_id,
|
||||||
|
site=site_id,
|
||||||
api = request.data["api"]
|
agent_type=request.data["agenttype"],
|
||||||
atype = request.data["agenttype"]
|
rdp=request.data["rdp"],
|
||||||
rdp = request.data["rdp"]
|
ping=request.data["ping"],
|
||||||
ping = request.data["ping"]
|
power=request.data["power"],
|
||||||
power = request.data["power"]
|
arch=arch,
|
||||||
|
token=token,
|
||||||
file_name = "rmm-installer.exe"
|
api=request.data["api"],
|
||||||
exe = os.path.join(settings.EXE_DIR, file_name)
|
file_name=request.data["fileName"],
|
||||||
|
|
||||||
if os.path.exists(exe):
|
|
||||||
try:
|
|
||||||
os.remove(exe)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(str(e))
|
|
||||||
|
|
||||||
goarch = "amd64" if arch == "64" else "386"
|
|
||||||
cmd = [
|
|
||||||
"env",
|
|
||||||
"GOOS=windows",
|
|
||||||
f"GOARCH={goarch}",
|
|
||||||
go_bin,
|
|
||||||
"build",
|
|
||||||
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
|
|
||||||
f"-X 'main.Api={api}'",
|
|
||||||
f"-X 'main.Client={client_id}'",
|
|
||||||
f"-X 'main.Site={site_id}'",
|
|
||||||
f"-X 'main.Atype={atype}'",
|
|
||||||
f"-X 'main.Rdp={rdp}'",
|
|
||||||
f"-X 'main.Ping={ping}'",
|
|
||||||
f"-X 'main.Power={power}'",
|
|
||||||
f"-X 'main.DownloadUrl={download_url}'",
|
|
||||||
f"-X 'main.Token={token}'\"",
|
|
||||||
"-o",
|
|
||||||
exe,
|
|
||||||
]
|
|
||||||
|
|
||||||
build_error = False
|
|
||||||
gen_error = False
|
|
||||||
|
|
||||||
gen = [
|
|
||||||
"env",
|
|
||||||
"GOOS=windows",
|
|
||||||
f"GOARCH={goarch}",
|
|
||||||
go_bin,
|
|
||||||
"generate",
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
r1 = subprocess.run(
|
|
||||||
" ".join(gen),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
gen_error = True
|
|
||||||
logger.error(str(e))
|
|
||||||
return Response(
|
|
||||||
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
|
|
||||||
)
|
|
||||||
|
|
||||||
if r1.returncode != 0:
|
|
||||||
gen_error = True
|
|
||||||
if r1.stdout:
|
|
||||||
logger.error(r1.stdout.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
if r1.stderr:
|
|
||||||
logger.error(r1.stderr.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
logger.error(f"Go build failed with return code {r1.returncode}")
|
|
||||||
|
|
||||||
if gen_error:
|
|
||||||
return Response(
|
|
||||||
"genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
" ".join(cmd),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
build_error = True
|
|
||||||
logger.error(str(e))
|
|
||||||
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
|
|
||||||
|
|
||||||
if r.returncode != 0:
|
|
||||||
build_error = True
|
|
||||||
if r.stdout:
|
|
||||||
logger.error(r.stdout.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
if r.stderr:
|
|
||||||
logger.error(r.stderr.decode("utf-8", errors="ignore"))
|
|
||||||
|
|
||||||
logger.error(f"Go build failed with return code {r.returncode}")
|
|
||||||
|
|
||||||
if build_error:
|
|
||||||
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
with open(exe, "rb") as f:
|
|
||||||
response = HttpResponse(
|
|
||||||
f.read(),
|
|
||||||
content_type="application/vnd.microsoft.portable-executable",
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = f"inline; filename={file_name}"
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
response = HttpResponse()
|
|
||||||
response["Content-Disposition"] = f"attachment; filename={file_name}"
|
|
||||||
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
|
|
||||||
return response
|
|
||||||
|
|
||||||
elif request.data["installMethod"] == "manual":
|
elif request.data["installMethod"] == "manual":
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -638,22 +489,14 @@ def recover(request):
|
|||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
mode = request.data["mode"]
|
mode = request.data["mode"]
|
||||||
|
|
||||||
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
|
# attempt a realtime recovery, otherwise fall back to old recovery method
|
||||||
return notify_error("Only available in agent version greater than 0.9.5")
|
|
||||||
|
|
||||||
if not agent.has_nats:
|
|
||||||
if mode == "tacagent" or mode == "rpc":
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
|
|
||||||
if agent.has_nats:
|
|
||||||
if mode == "tacagent" or mode == "mesh":
|
if mode == "tacagent" or mode == "mesh":
|
||||||
data = {"func": "recover", "payload": {"mode": mode}}
|
data = {"func": "recover", "payload": {"mode": mode}}
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
r = asyncio.run(agent.nats_cmd(data, timeout=10))
|
||||||
if r == "ok":
|
if r == "ok":
|
||||||
return Response("Successfully completed recovery")
|
return Response("Successfully completed recovery")
|
||||||
|
|
||||||
if agent.recoveryactions.filter(last_run=None).exists():
|
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
|
||||||
return notify_error(
|
return notify_error(
|
||||||
"A recovery action is currently pending. Please wait for the next agent check-in."
|
"A recovery action is currently pending. Please wait for the next agent check-in."
|
||||||
)
|
)
|
||||||
@@ -681,8 +524,6 @@ def recover(request):
|
|||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def run_script(request):
|
def run_script(request):
|
||||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
||||||
output = request.data["output"]
|
output = request.data["output"]
|
||||||
args = request.data["args"]
|
args = request.data["args"]
|
||||||
@@ -701,9 +542,6 @@ def run_script(request):
|
|||||||
return Response(r)
|
return Response(r)
|
||||||
|
|
||||||
elif output == "email":
|
elif output == "email":
|
||||||
if not pyver.parse(agent.version) >= pyver.parse("1.1.12"):
|
|
||||||
return notify_error("Requires agent version 1.1.12 or greater")
|
|
||||||
|
|
||||||
emails = (
|
emails = (
|
||||||
[] if request.data["emailmode"] == "default" else request.data["emails"]
|
[] if request.data["emailmode"] == "default" else request.data["emails"]
|
||||||
)
|
)
|
||||||
@@ -723,9 +561,6 @@ def run_script(request):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def recover_mesh(request, pk):
|
def recover_mesh(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||||
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
@@ -807,7 +642,7 @@ def bulk(request):
|
|||||||
elif request.data["monType"] == "workstations":
|
elif request.data["monType"] == "workstations":
|
||||||
q = q.filter(monitoring_type="workstation")
|
q = q.filter(monitoring_type="workstation")
|
||||||
|
|
||||||
agents: List[int] = [agent.pk for agent in q]
|
agents: list[int] = [agent.pk for agent in q]
|
||||||
|
|
||||||
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
|
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
|
||||||
|
|
||||||
@@ -836,49 +671,6 @@ def bulk(request):
|
|||||||
return notify_error("Something went wrong")
|
return notify_error("Something went wrong")
|
||||||
|
|
||||||
|
|
||||||
@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": server_offline_count,
|
|
||||||
"total_workstation_count": Agent.objects.filter(
|
|
||||||
monitoring_type="workstation"
|
|
||||||
).count(),
|
|
||||||
"total_workstation_offline_count": workstation_offline_count,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def agent_maintenance(request):
|
def agent_maintenance(request):
|
||||||
if request.data["type"] == "Client":
|
if request.data["type"] == "Client":
|
||||||
@@ -905,9 +697,6 @@ def agent_maintenance(request):
|
|||||||
class WMI(APIView):
|
class WMI(APIView):
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if pyver.parse(agent.version) < pyver.parse("1.1.2"):
|
|
||||||
return notify_error("Requires agent version 1.1.2 or greater")
|
|
||||||
|
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
|
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
|
||||||
if r != "ok":
|
if r != "ok":
|
||||||
return notify_error("Unable to contact the agent")
|
return notify_error("Unable to contact the agent")
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields import BooleanField, PositiveIntegerField
|
from django.db.models.fields import BooleanField, PositiveIntegerField
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
|
|
||||||
SEVERITY_CHOICES = [
|
SEVERITY_CHOICES = [
|
||||||
("info", "Informational"),
|
("info", "Informational"),
|
||||||
@@ -78,7 +91,7 @@ class Alert(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_availability_alert(cls, agent):
|
def create_or_return_availability_alert(cls, agent):
|
||||||
if not cls.objects.filter(agent=agent, resolved=False).exists():
|
if not cls.objects.filter(agent=agent, resolved=False).exists():
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
@@ -87,9 +100,11 @@ class Alert(models.Model):
|
|||||||
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
|
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
|
||||||
hidden=True,
|
hidden=True,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(agent=agent, resolved=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_check_alert(cls, check):
|
def create_or_return_check_alert(cls, check):
|
||||||
|
|
||||||
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
|
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
@@ -99,9 +114,11 @@ class Alert(models.Model):
|
|||||||
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
|
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
|
||||||
hidden=True,
|
hidden=True,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(assigned_check=check, resolved=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_task_alert(cls, task):
|
def create_or_return_task_alert(cls, task):
|
||||||
|
|
||||||
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
|
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
@@ -111,10 +128,305 @@ class Alert(models.Model):
|
|||||||
message=f"{task.agent.hostname} has task: {task.name} that failed.",
|
message=f"{task.agent.hostname} has task: {task.name} that failed.",
|
||||||
hidden=True,
|
hidden=True,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return cls.objects.get(assigned_task=task, resolved=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_custom_alert(cls, custom):
|
def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
# set variables
|
||||||
|
dashboard_severities = None
|
||||||
|
email_severities = None
|
||||||
|
text_severities = None
|
||||||
|
always_dashboard = None
|
||||||
|
always_email = None
|
||||||
|
always_text = None
|
||||||
|
alert_interval = None
|
||||||
|
email_task = None
|
||||||
|
text_task = None
|
||||||
|
|
||||||
|
# check what the instance passed is
|
||||||
|
if isinstance(instance, Agent):
|
||||||
|
from agents.tasks import agent_outage_email_task, agent_outage_sms_task
|
||||||
|
|
||||||
|
email_task = agent_outage_email_task
|
||||||
|
text_task = agent_outage_sms_task
|
||||||
|
|
||||||
|
email_alert = instance.overdue_email_alert
|
||||||
|
text_alert = instance.overdue_text_alert
|
||||||
|
dashboard_alert = instance.overdue_dashboard_alert
|
||||||
|
alert_template = instance.alert_template
|
||||||
|
maintenance_mode = instance.maintenance_mode
|
||||||
|
alert_severity = "error"
|
||||||
|
agent = instance
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = ["error"]
|
||||||
|
email_severities = ["error"]
|
||||||
|
text_severities = ["error"]
|
||||||
|
always_dashboard = alert_template.agent_always_alert
|
||||||
|
always_email = alert_template.agent_always_email
|
||||||
|
always_text = alert_template.agent_always_text
|
||||||
|
alert_interval = alert_template.agent_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_availability_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(agent=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(agent=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
|
||||||
|
elif isinstance(instance, Check):
|
||||||
|
from checks.tasks import (
|
||||||
|
handle_check_email_alert_task,
|
||||||
|
handle_check_sms_alert_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_task = handle_check_email_alert_task
|
||||||
|
text_task = handle_check_sms_alert_task
|
||||||
|
|
||||||
|
email_alert = instance.email_alert
|
||||||
|
text_alert = instance.text_alert
|
||||||
|
dashboard_alert = instance.dashboard_alert
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
alert_severity = instance.alert_severity
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = alert_template.check_dashboard_alert_severity
|
||||||
|
email_severities = alert_template.check_email_alert_severity
|
||||||
|
text_severities = alert_template.check_text_alert_severity
|
||||||
|
always_dashboard = alert_template.check_always_alert
|
||||||
|
always_email = alert_template.check_always_email
|
||||||
|
always_text = alert_template.check_always_text
|
||||||
|
alert_interval = alert_template.check_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_check_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(assigned_check=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(assigned_check=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
|
||||||
|
elif isinstance(instance, AutomatedTask):
|
||||||
|
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
|
||||||
|
|
||||||
|
email_task = handle_task_email_alert
|
||||||
|
text_task = handle_task_sms_alert
|
||||||
|
|
||||||
|
email_alert = instance.email_alert
|
||||||
|
text_alert = instance.text_alert
|
||||||
|
dashboard_alert = instance.dashboard_alert
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
alert_severity = instance.alert_severity
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
# set alert_template settings
|
||||||
|
if alert_template:
|
||||||
|
dashboard_severities = alert_template.task_dashboard_alert_severity
|
||||||
|
email_severities = alert_template.task_email_alert_severity
|
||||||
|
text_severities = alert_template.task_text_alert_severity
|
||||||
|
always_dashboard = alert_template.task_always_alert
|
||||||
|
always_email = alert_template.task_always_email
|
||||||
|
always_text = alert_template.task_always_text
|
||||||
|
alert_interval = alert_template.task_periodic_alert_days
|
||||||
|
|
||||||
|
if instance.should_create_alert(alert_template):
|
||||||
|
alert = cls.create_or_return_task_alert(instance)
|
||||||
|
else:
|
||||||
|
# check if there is an alert that exists
|
||||||
|
if cls.objects.filter(assigned_task=instance, resolved=False).exists():
|
||||||
|
alert = cls.objects.get(assigned_task=instance, resolved=False)
|
||||||
|
else:
|
||||||
|
alert = None
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if agent is in maintenance mode
|
||||||
|
if maintenance_mode or not alert:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check if alert severity changed on check and update the alert
|
||||||
|
if alert_severity != alert.severity:
|
||||||
|
alert.severity = alert_severity
|
||||||
|
alert.save(update_fields=["severity"])
|
||||||
|
|
||||||
|
# create alert in dashboard if enabled
|
||||||
|
if dashboard_alert or always_dashboard:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in dashboard_severities: # type: ignore
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
alert.hidden = False
|
||||||
|
alert.save()
|
||||||
|
|
||||||
|
# send email if enabled
|
||||||
|
if email_alert or always_email:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in email_severities: # type: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
email_task.delay(
|
||||||
|
pk=alert.pk,
|
||||||
|
alert_interval=alert_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
# send text if enabled
|
||||||
|
if text_alert or always_text:
|
||||||
|
|
||||||
|
# check if alert template is set and specific severities are configured
|
||||||
|
if alert_template and alert.severity not in text_severities: # type: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
|
||||||
|
|
||||||
|
# check if any scripts should be run
|
||||||
|
if alert_template and alert_template.action and not alert.action_run:
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=alert_template.action.pk,
|
||||||
|
args=alert_template.action_args,
|
||||||
|
timeout=alert_template.action_timeout,
|
||||||
|
wait=True,
|
||||||
|
full=True,
|
||||||
|
run_on_any=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# command was successful
|
||||||
|
if type(r) == dict:
|
||||||
|
alert.action_retcode = r["retcode"]
|
||||||
|
alert.action_stdout = r["stdout"]
|
||||||
|
alert.action_stderr = r["stderr"]
|
||||||
|
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
|
||||||
|
alert.action_run = djangotime.now()
|
||||||
|
alert.save()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
|
||||||
|
from agents.models import Agent
|
||||||
|
from autotasks.models import AutomatedTask
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
# set variables
|
||||||
|
email_on_resolved = False
|
||||||
|
text_on_resolved = False
|
||||||
|
resolved_email_task = None
|
||||||
|
resolved_text_task = None
|
||||||
|
|
||||||
|
# check what the instance passed is
|
||||||
|
if isinstance(instance, Agent):
|
||||||
|
from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
|
||||||
|
|
||||||
|
resolved_email_task = agent_recovery_email_task
|
||||||
|
resolved_text_task = agent_recovery_sms_task
|
||||||
|
|
||||||
|
alert_template = instance.alert_template
|
||||||
|
alert = cls.objects.get(agent=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.maintenance_mode
|
||||||
|
agent = instance
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.agent_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.agent_text_on_resolved
|
||||||
|
|
||||||
|
elif isinstance(instance, Check):
|
||||||
|
from checks.tasks import (
|
||||||
|
handle_resolved_check_email_alert_task,
|
||||||
|
handle_resolved_check_sms_alert_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_email_task = handle_resolved_check_email_alert_task
|
||||||
|
resolved_text_task = handle_resolved_check_sms_alert_task
|
||||||
|
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
alert = cls.objects.get(assigned_check=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.check_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.check_text_on_resolved
|
||||||
|
|
||||||
|
elif isinstance(instance, AutomatedTask):
|
||||||
|
from autotasks.tasks import (
|
||||||
|
handle_resolved_task_email_alert,
|
||||||
|
handle_resolved_task_sms_alert,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_email_task = handle_resolved_task_email_alert
|
||||||
|
resolved_text_task = handle_resolved_task_sms_alert
|
||||||
|
|
||||||
|
alert_template = instance.agent.alert_template
|
||||||
|
alert = cls.objects.get(assigned_task=instance, resolved=False)
|
||||||
|
maintenance_mode = instance.agent.maintenance_mode
|
||||||
|
agent = instance.agent
|
||||||
|
|
||||||
|
if alert_template:
|
||||||
|
email_on_resolved = alert_template.task_email_on_resolved
|
||||||
|
text_on_resolved = alert_template.task_text_on_resolved
|
||||||
|
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# return if agent is in maintenance mode
|
||||||
|
if maintenance_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
alert.resolve()
|
||||||
|
|
||||||
|
# check if a resolved email notification should be send
|
||||||
|
if email_on_resolved and not alert.resolved_email_sent:
|
||||||
|
resolved_email_task.delay(pk=alert.pk)
|
||||||
|
|
||||||
|
# check if resolved text should be sent
|
||||||
|
if text_on_resolved and not alert.resolved_sms_sent:
|
||||||
|
resolved_text_task.delay(pk=alert.pk)
|
||||||
|
|
||||||
|
# check if resolved script should be run
|
||||||
|
if (
|
||||||
|
alert_template
|
||||||
|
and alert_template.resolved_action
|
||||||
|
and not alert.resolved_action_run
|
||||||
|
):
|
||||||
|
r = agent.run_script(
|
||||||
|
scriptpk=alert_template.resolved_action.pk,
|
||||||
|
args=alert_template.resolved_action_args,
|
||||||
|
timeout=alert_template.resolved_action_timeout,
|
||||||
|
wait=True,
|
||||||
|
full=True,
|
||||||
|
run_on_any=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# command was successful
|
||||||
|
if type(r) == dict:
|
||||||
|
alert.resolved_action_retcode = r["retcode"]
|
||||||
|
alert.resolved_action_stdout = r["stdout"]
|
||||||
|
alert.resolved_action_stderr = r["stderr"]
|
||||||
|
alert.resolved_action_execution_time = "{:.4f}".format(
|
||||||
|
r["execution_time"]
|
||||||
|
)
|
||||||
|
alert.resolved_action_run = djangotime.now()
|
||||||
|
alert.save()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlertTemplate(models.Model):
|
class AlertTemplate(models.Model):
|
||||||
@@ -283,4 +595,4 @@ class AlertTemplate(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_default_template(self) -> bool:
|
def is_default_template(self) -> bool:
|
||||||
return self.default_alert_template.exists()
|
return self.default_alert_template.exists() # type: ignore
|
||||||
|
|||||||
@@ -12,3 +12,13 @@ def unsnooze_alerts() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def cache_agents_alert_template():
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
for agent in Agent.objects.only("pk"):
|
||||||
|
agent.set_alert_template()
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ from .serializers import (
|
|||||||
AlertTemplateRelationSerializer,
|
AlertTemplateRelationSerializer,
|
||||||
AlertTemplateSerializer,
|
AlertTemplateSerializer,
|
||||||
)
|
)
|
||||||
|
from .tasks import cache_agents_alert_template
|
||||||
|
|
||||||
|
|
||||||
class GetAddAlerts(APIView):
|
class GetAddAlerts(APIView):
|
||||||
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
def delete(self, request, pk):
|
def delete(self, request, pk):
|
||||||
get_object_or_404(AlertTemplate, pk=pk).delete()
|
get_object_or_404(AlertTemplate, pk=pk).delete()
|
||||||
|
|
||||||
|
# cache alert_template value on agents
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from itertools import cycle
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
@@ -18,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
|
|||||||
def test_get_checks(self):
|
def test_get_checks(self):
|
||||||
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
|
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
|
||||||
|
|
||||||
|
# add a check
|
||||||
|
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
|
||||||
|
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
|
||||||
|
|
||||||
|
# override check run interval
|
||||||
|
check2 = baker.make_recipe(
|
||||||
|
"checks.ping_check", agent=self.agent, run_interval=20
|
||||||
|
)
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
|
||||||
|
|
||||||
|
# Set last_run on both checks and should return an empty list
|
||||||
|
check1.last_run = djangotime.now()
|
||||||
|
check1.save()
|
||||||
|
check2.last_run = djangotime.now()
|
||||||
|
check2.save()
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertFalse(r.data["checks"]) # type: ignore
|
||||||
|
|
||||||
|
# set last_run greater than interval
|
||||||
|
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||||
|
check1.save()
|
||||||
|
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
|
||||||
|
check2.save()
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data["check_interval"], 20) # type: ignore
|
||||||
|
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
|
||||||
|
|
||||||
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
@@ -53,3 +89,117 @@ class TestAPIv3(TacticalTestCase):
|
|||||||
r.json(),
|
r.json(),
|
||||||
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
|
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# add check to agent with check interval set
|
||||||
|
check = baker.make_recipe(
|
||||||
|
"checks.ping_check", agent=self.agent, run_interval=30
|
||||||
|
)
|
||||||
|
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(),
|
||||||
|
{"agent": self.agent.pk, "check_interval": 30},
|
||||||
|
)
|
||||||
|
|
||||||
|
# minimum check run interval is 15 seconds
|
||||||
|
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
|
||||||
|
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(),
|
||||||
|
{"agent": self.agent.pk, "check_interval": 15},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_checks(self):
|
||||||
|
# force run all checks regardless of interval
|
||||||
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
|
baker.make_recipe("checks.ping_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.cpuload_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.memory_check", agent=agent)
|
||||||
|
baker.make_recipe("checks.eventlog_check", agent=agent)
|
||||||
|
for _ in range(10):
|
||||||
|
baker.make_recipe("checks.script_check", agent=agent)
|
||||||
|
|
||||||
|
url = f"/api/v3/{agent.agent_id}/runchecks/"
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.json()["agent"], agent.pk)
|
||||||
|
self.assertIsInstance(r.json()["check_interval"], int)
|
||||||
|
self.assertEqual(len(r.json()["checks"]), 15)
|
||||||
|
|
||||||
|
def test_checkin_patch(self):
|
||||||
|
from logs.models import PendingAction
|
||||||
|
|
||||||
|
url = "/api/v3/checkin/"
|
||||||
|
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
|
||||||
|
PendingAction.objects.create(
|
||||||
|
agent=agent_updated,
|
||||||
|
action_type="agentupdate",
|
||||||
|
details={
|
||||||
|
"url": agent_updated.winagent_dl,
|
||||||
|
"version": agent_updated.version,
|
||||||
|
"inno": agent_updated.win_inno_exe,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "pending")
|
||||||
|
|
||||||
|
# test agent failed to update and still on same version
|
||||||
|
payload = {
|
||||||
|
"func": "hello",
|
||||||
|
"agent_id": agent_updated.agent_id,
|
||||||
|
"version": "1.3.0",
|
||||||
|
}
|
||||||
|
r = self.client.patch(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "pending")
|
||||||
|
|
||||||
|
# test agent successful update
|
||||||
|
payload["version"] = settings.LATEST_AGENT_VER
|
||||||
|
r = self.client.patch(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||||
|
self.assertEqual(action.status, "completed")
|
||||||
|
action.delete()
|
||||||
|
|
||||||
|
@patch("apiv3.views.reload_nats")
|
||||||
|
def test_agent_recovery(self, reload_nats):
|
||||||
|
reload_nats.return_value = "ok"
|
||||||
|
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
|
url = f"/api/v3/{agent.agent_id}/recovery/"
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make(
|
||||||
|
"agents.RecoveryAction",
|
||||||
|
agent=agent,
|
||||||
|
mode="command",
|
||||||
|
command="shutdown /r /t 5 /f",
|
||||||
|
)
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(
|
||||||
|
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
|
||||||
|
)
|
||||||
|
reload_nats.assert_not_called()
|
||||||
|
|
||||||
|
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
|
||||||
|
reload_nats.assert_called_once()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("checkrunner/", views.CheckRunner.as_view()),
|
path("checkrunner/", views.CheckRunner.as_view()),
|
||||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
||||||
|
path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
|
||||||
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
|
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
|
||||||
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
|
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
|
||||||
path("meshexe/", views.MeshExe.as_view()),
|
path("meshexe/", views.MeshExe.as_view()),
|
||||||
@@ -17,4 +18,6 @@ urlpatterns = [
|
|||||||
path("choco/", views.Choco.as_view()),
|
path("choco/", views.Choco.as_view()),
|
||||||
path("winupdates/", views.WinUpdates.as_view()),
|
path("winupdates/", views.WinUpdates.as_view()),
|
||||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||||
|
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
|
||||||
|
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
|
|||||||
from checks.models import Check
|
from checks.models import Check
|
||||||
from checks.serializers import CheckRunnerGetSerializer
|
from checks.serializers import CheckRunnerGetSerializer
|
||||||
from checks.utils import bytes2human
|
from checks.utils import bytes2human
|
||||||
|
from logs.models import PendingAction
|
||||||
from software.models import InstalledSoftware
|
from software.models import InstalledSoftware
|
||||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
|
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
|
||||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||||
@@ -35,6 +36,8 @@ class CheckIn(APIView):
|
|||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
|
from alerts.models import Alert
|
||||||
|
|
||||||
updated = False
|
updated = False
|
||||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||||
if pyver.parse(request.data["version"]) > pyver.parse(
|
if pyver.parse(request.data["version"]) > pyver.parse(
|
||||||
@@ -50,26 +53,20 @@ class CheckIn(APIView):
|
|||||||
# change agent update pending status to completed if agent has just updated
|
# change agent update pending status to completed if agent has just updated
|
||||||
if (
|
if (
|
||||||
updated
|
updated
|
||||||
and agent.pendingactions.filter(
|
and agent.pendingactions.filter( # type: ignore
|
||||||
action_type="agentupdate", status="pending"
|
action_type="agentupdate", status="pending"
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
agent.pendingactions.filter(
|
agent.pendingactions.filter( # type: ignore
|
||||||
action_type="agentupdate", status="pending"
|
action_type="agentupdate", status="pending"
|
||||||
).update(status="completed")
|
).update(status="completed")
|
||||||
|
|
||||||
# handles any alerting actions
|
# handles any alerting actions
|
||||||
agent.handle_alert(checkin=True)
|
if Alert.objects.filter(agent=agent, resolved=False).exists():
|
||||||
|
Alert.handle_alert_resolve(agent)
|
||||||
recovery = agent.recoveryactions.filter(last_run=None).last()
|
|
||||||
if recovery is not None:
|
|
||||||
recovery.last_run = djangotime.now()
|
|
||||||
recovery.save(update_fields=["last_run"])
|
|
||||||
handle_agent_recovery_task.delay(pk=recovery.pk)
|
|
||||||
return Response("ok")
|
|
||||||
|
|
||||||
# get any pending actions
|
# get any pending actions
|
||||||
if agent.pendingactions.filter(status="pending").exists():
|
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
|
||||||
agent.handle_pending_actions()
|
agent.handle_pending_actions()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
@@ -111,7 +108,7 @@ class CheckIn(APIView):
|
|||||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||||
InstalledSoftware(agent=agent, software=sw).save()
|
InstalledSoftware(agent=agent, software=sw).save()
|
||||||
else:
|
else:
|
||||||
s = agent.installedsoftware_set.first()
|
s = agent.installedsoftware_set.first() # type: ignore
|
||||||
s.software = sw
|
s.software = sw
|
||||||
s.save(update_fields=["software"])
|
s.save(update_fields=["software"])
|
||||||
|
|
||||||
@@ -184,7 +181,7 @@ class WinUpdates(APIView):
|
|||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||||
u = agent.winupdates.filter(guid=request.data["guid"]).last()
|
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
|
||||||
success: bool = request.data["success"]
|
success: bool = request.data["success"]
|
||||||
if success:
|
if success:
|
||||||
u.result = "success"
|
u.result = "success"
|
||||||
@@ -210,8 +207,8 @@ class WinUpdates(APIView):
|
|||||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||||
updates = request.data["wua_updates"]
|
updates = request.data["wua_updates"]
|
||||||
for update in updates:
|
for update in updates:
|
||||||
if agent.winupdates.filter(guid=update["guid"]).exists():
|
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
|
||||||
u = agent.winupdates.filter(guid=update["guid"]).last()
|
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
|
||||||
u.downloaded = update["downloaded"]
|
u.downloaded = update["downloaded"]
|
||||||
u.installed = update["installed"]
|
u.installed = update["installed"]
|
||||||
u.save(update_fields=["downloaded", "installed"])
|
u.save(update_fields=["downloaded", "installed"])
|
||||||
@@ -242,7 +239,7 @@ class WinUpdates(APIView):
|
|||||||
|
|
||||||
# more superseded updates cleanup
|
# more superseded updates cleanup
|
||||||
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
|
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
|
||||||
for u in agent.winupdates.filter(
|
for u in agent.winupdates.filter( # type: ignore
|
||||||
date_installed__isnull=True, result="failed"
|
date_installed__isnull=True, result="failed"
|
||||||
).exclude(installed=True):
|
).exclude(installed=True):
|
||||||
u.delete()
|
u.delete()
|
||||||
@@ -256,25 +253,20 @@ class SupersededWinUpdate(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||||
updates = agent.winupdates.filter(guid=request.data["guid"])
|
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
|
||||||
for u in updates:
|
for u in updates:
|
||||||
u.delete()
|
u.delete()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
class CheckRunner(APIView):
|
class RunChecks(APIView):
|
||||||
"""
|
|
||||||
For the windows golang agent
|
|
||||||
"""
|
|
||||||
|
|
||||||
authentication_classes = [TokenAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, agentid):
|
def get(self, request, agentid):
|
||||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||||
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
|
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"agent": agent.pk,
|
"agent": agent.pk,
|
||||||
"check_interval": agent.check_interval,
|
"check_interval": agent.check_interval,
|
||||||
@@ -282,6 +274,42 @@ class CheckRunner(APIView):
|
|||||||
}
|
}
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckRunner(APIView):
|
||||||
|
authentication_classes = [TokenAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request, agentid):
|
||||||
|
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||||
|
checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
|
||||||
|
|
||||||
|
run_list = [
|
||||||
|
check
|
||||||
|
for check in checks
|
||||||
|
# always run if check hasn't run yet
|
||||||
|
if not check.last_run
|
||||||
|
# if a check interval is set, see if the correct amount of seconds have passed
|
||||||
|
or (
|
||||||
|
check.run_interval
|
||||||
|
and (
|
||||||
|
check.last_run
|
||||||
|
< djangotime.now()
|
||||||
|
- djangotime.timedelta(seconds=check.run_interval)
|
||||||
|
)
|
||||||
|
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
check.last_run
|
||||||
|
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
ret = {
|
||||||
|
"agent": agent.pk,
|
||||||
|
"check_interval": agent.check_run_interval(),
|
||||||
|
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
|
||||||
|
}
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
check = get_object_or_404(Check, pk=request.data["id"])
|
check = get_object_or_404(Check, pk=request.data["id"])
|
||||||
check.last_run = djangotime.now()
|
check.last_run = djangotime.now()
|
||||||
@@ -297,14 +325,13 @@ class CheckRunnerInterval(APIView):
|
|||||||
|
|
||||||
def get(self, request, agentid):
|
def get(self, request, agentid):
|
||||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||||
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
|
|
||||||
|
return Response(
|
||||||
|
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskRunner(APIView):
|
class TaskRunner(APIView):
|
||||||
"""
|
|
||||||
For the windows golang agent
|
|
||||||
"""
|
|
||||||
|
|
||||||
authentication_classes = [TokenAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@@ -314,6 +341,7 @@ class TaskRunner(APIView):
|
|||||||
return Response(TaskGOGetSerializer(task).data)
|
return Response(TaskGOGetSerializer(task).data)
|
||||||
|
|
||||||
def patch(self, request, pk, agentid):
|
def patch(self, request, pk, agentid):
|
||||||
|
from alerts.models import Alert
|
||||||
from logs.models import AuditLog
|
from logs.models import AuditLog
|
||||||
|
|
||||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||||
@@ -325,8 +353,17 @@ class TaskRunner(APIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save(last_run=djangotime.now())
|
serializer.save(last_run=djangotime.now())
|
||||||
|
|
||||||
new_task = AutomatedTask.objects.get(pk=task.pk)
|
status = "failing" if task.retcode != 0 else "passing"
|
||||||
new_task.handle_alert()
|
|
||||||
|
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
|
||||||
|
new_task.status = status
|
||||||
|
new_task.save()
|
||||||
|
|
||||||
|
if status == "passing":
|
||||||
|
if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
|
||||||
|
Alert.handle_alert_resolve(new_task)
|
||||||
|
else:
|
||||||
|
Alert.handle_alert_failure(new_task)
|
||||||
|
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
username=agent.hostname,
|
username=agent.hostname,
|
||||||
@@ -404,10 +441,10 @@ class NewAgent(APIView):
|
|||||||
agent.salt_id = f"{agent.hostname}-{agent.pk}"
|
agent.salt_id = f"{agent.hostname}-{agent.pk}"
|
||||||
agent.save(update_fields=["salt_id"])
|
agent.save(update_fields=["salt_id"])
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user( # type: ignore
|
||||||
username=request.data["agent_id"],
|
username=request.data["agent_id"],
|
||||||
agent=agent,
|
agent=agent,
|
||||||
password=User.objects.make_random_password(60),
|
password=User.objects.make_random_password(60), # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
token = Token.objects.create(user=user)
|
token = Token.objects.create(user=user)
|
||||||
@@ -452,7 +489,7 @@ class Software(APIView):
|
|||||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||||
InstalledSoftware(agent=agent, software=sw).save()
|
InstalledSoftware(agent=agent, software=sw).save()
|
||||||
else:
|
else:
|
||||||
s = agent.installedsoftware_set.first()
|
s = agent.installedsoftware_set.first() # type: ignore
|
||||||
s.software = sw
|
s.software = sw
|
||||||
s.save(update_fields=["software"])
|
s.save(update_fields=["software"])
|
||||||
|
|
||||||
@@ -475,3 +512,59 @@ class Installer(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class ChocoResult(APIView):
|
||||||
|
authentication_classes = [TokenAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
action = get_object_or_404(PendingAction, pk=pk)
|
||||||
|
results: str = request.data["results"]
|
||||||
|
|
||||||
|
software_name = action.details["name"].lower()
|
||||||
|
success = [
|
||||||
|
"install",
|
||||||
|
"of",
|
||||||
|
software_name,
|
||||||
|
"was",
|
||||||
|
"successful",
|
||||||
|
"installed",
|
||||||
|
]
|
||||||
|
duplicate = [software_name, "already", "installed", "--force", "reinstall"]
|
||||||
|
installed = False
|
||||||
|
|
||||||
|
if all(x in results.lower() for x in success):
|
||||||
|
installed = True
|
||||||
|
elif all(x in results.lower() for x in duplicate):
|
||||||
|
installed = True
|
||||||
|
|
||||||
|
action.details["output"] = results
|
||||||
|
action.details["installed"] = installed
|
||||||
|
action.status = "completed"
|
||||||
|
action.save(update_fields=["details", "status"])
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRecovery(APIView):
|
||||||
|
authentication_classes = [TokenAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request, agentid):
|
||||||
|
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||||
|
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
|
||||||
|
ret = {"mode": "pass", "shellcmd": ""}
|
||||||
|
if recovery is None:
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
recovery.last_run = djangotime.now()
|
||||||
|
recovery.save(update_fields=["last_run"])
|
||||||
|
|
||||||
|
ret["mode"] = recovery.mode
|
||||||
|
|
||||||
|
if recovery.mode == "command":
|
||||||
|
ret["shellcmd"] = recovery.command
|
||||||
|
elif recovery.mode == "rpc":
|
||||||
|
reload_nats()
|
||||||
|
|
||||||
|
return Response(ret)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-02 04:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agents', '0030_agent_offline_time'),
|
||||||
|
('clients', '0009_auto_20210212_1408'),
|
||||||
|
('automation', '0007_policy_alert_template'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='policy',
|
||||||
|
name='excluded_agents',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='policy',
|
||||||
|
name='excluded_clients',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='policy',
|
||||||
|
name='excluded_sites',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -17,8 +17,18 @@ class Policy(BaseAuditModel):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
excluded_sites = models.ManyToManyField(
|
||||||
|
"clients.Site", related_name="policy_exclusions", blank=True
|
||||||
|
)
|
||||||
|
excluded_clients = models.ManyToManyField(
|
||||||
|
"clients.Client", related_name="policy_exclusions", blank=True
|
||||||
|
)
|
||||||
|
excluded_agents = models.ManyToManyField(
|
||||||
|
"agents.Agent", related_name="policy_exclusions", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
from alerts.tasks import cache_agents_alert_template
|
||||||
from automation.tasks import generate_agent_checks_from_policies_task
|
from automation.tasks import generate_agent_checks_from_policies_task
|
||||||
|
|
||||||
# get old policy if exists
|
# get old policy if exists
|
||||||
@@ -33,6 +43,9 @@ class Policy(BaseAuditModel):
|
|||||||
create_tasks=True,
|
create_tasks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if old_policy.alert_template != self.alert_template:
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
from automation.tasks import generate_agent_checks_task
|
from automation.tasks import generate_agent_checks_task
|
||||||
|
|
||||||
@@ -43,28 +56,50 @@ class Policy(BaseAuditModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_default_server_policy(self):
|
def is_default_server_policy(self):
|
||||||
return self.default_server_policy.exists()
|
return self.default_server_policy.exists() # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_default_workstation_policy(self):
|
def is_default_workstation_policy(self):
|
||||||
return self.default_workstation_policy.exists()
|
return self.default_workstation_policy.exists() # type: ignore
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def is_agent_excluded(self, agent):
|
||||||
|
return (
|
||||||
|
agent in self.excluded_agents.all()
|
||||||
|
or agent.site in self.excluded_sites.all()
|
||||||
|
or agent.client in self.excluded_clients.all()
|
||||||
|
)
|
||||||
|
|
||||||
def related_agents(self):
|
def related_agents(self):
|
||||||
return self.get_related("server") | self.get_related("workstation")
|
return self.get_related("server") | self.get_related("workstation")
|
||||||
|
|
||||||
def get_related(self, mon_type):
|
def get_related(self, mon_type):
|
||||||
explicit_agents = self.agents.filter(monitoring_type=mon_type)
|
explicit_agents = (
|
||||||
explicit_clients = getattr(self, f"{mon_type}_clients").all()
|
self.agents.filter(monitoring_type=mon_type) # type: ignore
|
||||||
explicit_sites = getattr(self, f"{mon_type}_sites").all()
|
.exclude(
|
||||||
|
pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
.exclude(site__in=self.excluded_sites.all())
|
||||||
|
.exclude(site__client__in=self.excluded_clients.all())
|
||||||
|
)
|
||||||
|
|
||||||
|
explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
|
||||||
|
pk__in=self.excluded_clients.all()
|
||||||
|
)
|
||||||
|
explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
|
||||||
|
pk__in=self.excluded_sites.all()
|
||||||
|
)
|
||||||
|
|
||||||
filtered_agents_pks = Policy.objects.none()
|
filtered_agents_pks = Policy.objects.none()
|
||||||
|
|
||||||
filtered_agents_pks |= Agent.objects.filter(
|
filtered_agents_pks |= Agent.objects.filter(
|
||||||
site__in=[
|
site__in=[
|
||||||
site for site in explicit_sites if site.client not in explicit_clients
|
site
|
||||||
|
for site in explicit_sites
|
||||||
|
if site.client not in explicit_clients
|
||||||
|
and site.client not in self.excluded_clients.all()
|
||||||
],
|
],
|
||||||
monitoring_type=mon_type,
|
monitoring_type=mon_type,
|
||||||
).values_list("pk", flat=True)
|
).values_list("pk", flat=True)
|
||||||
@@ -119,23 +154,39 @@ class Policy(BaseAuditModel):
|
|||||||
client_policy = client.workstation_policy
|
client_policy = client.workstation_policy
|
||||||
site_policy = site.workstation_policy
|
site_policy = site.workstation_policy
|
||||||
|
|
||||||
if agent_policy and agent_policy.active:
|
if (
|
||||||
|
agent_policy
|
||||||
|
and agent_policy.active
|
||||||
|
and not agent_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
for task in agent_policy.autotasks.all():
|
for task in agent_policy.autotasks.all():
|
||||||
if task.pk not in added_task_pks:
|
if task.pk not in added_task_pks:
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
added_task_pks.append(task.pk)
|
added_task_pks.append(task.pk)
|
||||||
if site_policy and site_policy.active:
|
if (
|
||||||
|
site_policy
|
||||||
|
and site_policy.active
|
||||||
|
and not site_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
for task in site_policy.autotasks.all():
|
for task in site_policy.autotasks.all():
|
||||||
if task.pk not in added_task_pks:
|
if task.pk not in added_task_pks:
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
added_task_pks.append(task.pk)
|
added_task_pks.append(task.pk)
|
||||||
if client_policy and client_policy.active:
|
if (
|
||||||
|
client_policy
|
||||||
|
and client_policy.active
|
||||||
|
and not client_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
for task in client_policy.autotasks.all():
|
for task in client_policy.autotasks.all():
|
||||||
if task.pk not in added_task_pks:
|
if task.pk not in added_task_pks:
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
added_task_pks.append(task.pk)
|
added_task_pks.append(task.pk)
|
||||||
|
|
||||||
if default_policy and default_policy.active:
|
if (
|
||||||
|
default_policy
|
||||||
|
and default_policy.active
|
||||||
|
and not default_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
for task in default_policy.autotasks.all():
|
for task in default_policy.autotasks.all():
|
||||||
if task.pk not in added_task_pks:
|
if task.pk not in added_task_pks:
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
@@ -205,7 +256,11 @@ class Policy(BaseAuditModel):
|
|||||||
enforced_checks = list()
|
enforced_checks = list()
|
||||||
policy_checks = list()
|
policy_checks = list()
|
||||||
|
|
||||||
if agent_policy and agent_policy.active:
|
if (
|
||||||
|
agent_policy
|
||||||
|
and agent_policy.active
|
||||||
|
and not agent_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
if agent_policy.enforced:
|
if agent_policy.enforced:
|
||||||
for check in agent_policy.policychecks.all():
|
for check in agent_policy.policychecks.all():
|
||||||
enforced_checks.append(check)
|
enforced_checks.append(check)
|
||||||
@@ -213,7 +268,11 @@ class Policy(BaseAuditModel):
|
|||||||
for check in agent_policy.policychecks.all():
|
for check in agent_policy.policychecks.all():
|
||||||
policy_checks.append(check)
|
policy_checks.append(check)
|
||||||
|
|
||||||
if site_policy and site_policy.active:
|
if (
|
||||||
|
site_policy
|
||||||
|
and site_policy.active
|
||||||
|
and not site_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
if site_policy.enforced:
|
if site_policy.enforced:
|
||||||
for check in site_policy.policychecks.all():
|
for check in site_policy.policychecks.all():
|
||||||
enforced_checks.append(check)
|
enforced_checks.append(check)
|
||||||
@@ -221,7 +280,11 @@ class Policy(BaseAuditModel):
|
|||||||
for check in site_policy.policychecks.all():
|
for check in site_policy.policychecks.all():
|
||||||
policy_checks.append(check)
|
policy_checks.append(check)
|
||||||
|
|
||||||
if client_policy and client_policy.active:
|
if (
|
||||||
|
client_policy
|
||||||
|
and client_policy.active
|
||||||
|
and not client_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
if client_policy.enforced:
|
if client_policy.enforced:
|
||||||
for check in client_policy.policychecks.all():
|
for check in client_policy.policychecks.all():
|
||||||
enforced_checks.append(check)
|
enforced_checks.append(check)
|
||||||
@@ -229,7 +292,11 @@ class Policy(BaseAuditModel):
|
|||||||
for check in client_policy.policychecks.all():
|
for check in client_policy.policychecks.all():
|
||||||
policy_checks.append(check)
|
policy_checks.append(check)
|
||||||
|
|
||||||
if default_policy and default_policy.active:
|
if (
|
||||||
|
default_policy
|
||||||
|
and default_policy.active
|
||||||
|
and not default_policy.is_agent_excluded(agent)
|
||||||
|
):
|
||||||
if default_policy.enforced:
|
if default_policy.enforced:
|
||||||
for check in default_policy.policychecks.all():
|
for check in default_policy.policychecks.all():
|
||||||
enforced_checks.append(check)
|
enforced_checks.append(check)
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ from rest_framework.serializers import (
|
|||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from agents.serializers import AgentHostnameSerializer
|
||||||
from autotasks.models import AutomatedTask
|
from autotasks.models import AutomatedTask
|
||||||
from checks.models import Check
|
from checks.models import Check
|
||||||
from clients.models import Client
|
from clients.models import Client
|
||||||
|
from clients.serializers import ClientSerializer, SiteSerializer
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
from .models import Policy
|
from .models import Policy
|
||||||
@@ -25,6 +27,9 @@ class PolicyTableSerializer(ModelSerializer):
|
|||||||
agents_count = SerializerMethodField(read_only=True)
|
agents_count = SerializerMethodField(read_only=True)
|
||||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||||
alert_template = ReadOnlyField(source="alert_template.id")
|
alert_template = ReadOnlyField(source="alert_template.id")
|
||||||
|
excluded_clients = ClientSerializer(many=True)
|
||||||
|
excluded_sites = SiteSerializer(many=True)
|
||||||
|
excluded_agents = AgentHostnameSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Policy
|
model = Policy
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
|
|||||||
error_threshold=check.error_threshold,
|
error_threshold=check.error_threshold,
|
||||||
alert_severity=check.alert_severity,
|
alert_severity=check.alert_severity,
|
||||||
name=check.name,
|
name=check.name,
|
||||||
|
run_interval=check.run_interval,
|
||||||
disk=check.disk,
|
disk=check.disk,
|
||||||
fails_b4_alert=check.fails_b4_alert,
|
fails_b4_alert=check.fails_b4_alert,
|
||||||
ip=check.ip,
|
ip=check.ip,
|
||||||
@@ -98,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
|
|||||||
event_message=check.event_message,
|
event_message=check.event_message,
|
||||||
fail_when=check.fail_when,
|
fail_when=check.fail_when,
|
||||||
search_last_days=check.search_last_days,
|
search_last_days=check.search_last_days,
|
||||||
|
number_of_events_b4_alert=check.number_of_events_b4_alert,
|
||||||
email_alert=check.email_alert,
|
email_alert=check.email_alert,
|
||||||
text_alert=check.text_alert,
|
text_alert=check.text_alert,
|
||||||
dashboard_alert=check.dashboard_alert,
|
dashboard_alert=check.dashboard_alert,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
|
|||||||
from model_bakery import baker, seq
|
from model_bakery import baker, seq
|
||||||
|
|
||||||
from agents.models import Agent
|
from agents.models import Agent
|
||||||
|
from core.models import CoreSettings
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
from winupdate.models import WinUpdatePolicy
|
from winupdate.models import WinUpdatePolicy
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
serializer = PolicyTableSerializer(policies, many=True)
|
serializer = PolicyTableSerializer(policies, many=True)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -41,13 +42,13 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
self.assertEqual(resp.status_code, 404)
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
url = f"/automation/policies/{policy.pk}/"
|
url = f"/automation/policies/{policy.pk}/" # type: ignore
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
resp = self.client.get(url, format="json")
|
||||||
serializer = PolicySerializer(policy)
|
serializer = PolicySerializer(policy)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -79,13 +80,13 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
"desc": "policy desc",
|
"desc": "policy desc",
|
||||||
"active": True,
|
"active": True,
|
||||||
"enforced": False,
|
"enforced": False,
|
||||||
"copyId": policy.pk,
|
"copyId": policy.pk, # type: ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.post(f"/automation/policies/", data, format="json")
|
resp = self.client.post(f"/automation/policies/", data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(policy.autotasks.count(), 3)
|
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
|
||||||
self.assertEqual(policy.policychecks.count(), 7)
|
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
self.assertEqual(resp.status_code, 404)
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
policy = baker.make("automation.Policy", active=True, enforced=False)
|
policy = baker.make("automation.Policy", active=True, enforced=False)
|
||||||
url = f"/automation/policies/{policy.pk}/"
|
url = f"/automation/policies/{policy.pk}/" # type: ignore
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"name": "Test Policy Update",
|
"name": "Test Policy Update",
|
||||||
@@ -121,7 +122,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
resp = self.client.put(url, data, format="json")
|
resp = self.client.put(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
generate_agent_checks_from_policies_task.assert_called_with(
|
generate_agent_checks_from_policies_task.assert_called_with(
|
||||||
policypk=policy.pk, create_tasks=True
|
policypk=policy.pk, create_tasks=True # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
@@ -138,7 +139,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
agents = baker.make_recipe(
|
agents = baker.make_recipe(
|
||||||
"agents.agent", site=site, policy=policy, _quantity=3
|
"agents.agent", site=site, policy=policy, _quantity=3
|
||||||
)
|
)
|
||||||
url = f"/automation/policies/{policy.pk}/"
|
url = f"/automation/policies/{policy.pk}/" # type: ignore
|
||||||
|
|
||||||
resp = self.client.delete(url, format="json")
|
resp = self.client.delete(url, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
@@ -153,14 +154,14 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
# create policy with tasks
|
# create policy with tasks
|
||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||||
url = f"/automation/{policy.pk}/policyautomatedtasks/"
|
url = f"/automation/{policy.pk}/policyautomatedtasks/" # type: ignore
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
resp = self.client.get(url, format="json")
|
||||||
serializer = AutoTasksFieldSerializer(tasks, many=True)
|
serializer = AutoTasksFieldSerializer(tasks, many=True)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.assertEqual(len(resp.data), 3)
|
self.assertEqual(len(resp.data), 3) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -170,14 +171,14 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
checks = self.create_checks(policy=policy)
|
checks = self.create_checks(policy=policy)
|
||||||
|
|
||||||
url = f"/automation/{policy.pk}/policychecks/"
|
url = f"/automation/{policy.pk}/policychecks/" # type: ignore
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
resp = self.client.get(url, format="json")
|
||||||
serializer = PolicyCheckSerializer(checks, many=True)
|
serializer = PolicyCheckSerializer(checks, many=True)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.assertEqual(len(resp.data), 7)
|
self.assertEqual(len(resp.data), 7) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -199,7 +200,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
|
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
def test_policy_overview(self):
|
def test_policy_overview(self):
|
||||||
@@ -212,40 +213,40 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
clients = baker.make(
|
clients = baker.make(
|
||||||
"clients.Client",
|
"clients.Client",
|
||||||
server_policy=cycle(policies),
|
server_policy=cycle(policies), # type: ignore
|
||||||
workstation_policy=cycle(policies),
|
workstation_policy=cycle(policies), # type: ignore
|
||||||
_quantity=5,
|
_quantity=5,
|
||||||
)
|
)
|
||||||
baker.make(
|
baker.make(
|
||||||
"clients.Site",
|
"clients.Site",
|
||||||
client=cycle(clients),
|
client=cycle(clients), # type: ignore
|
||||||
server_policy=cycle(policies),
|
server_policy=cycle(policies), # type: ignore
|
||||||
workstation_policy=cycle(policies),
|
workstation_policy=cycle(policies), # type: ignore
|
||||||
_quantity=4,
|
_quantity=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
baker.make("clients.Site", client=cycle(clients), _quantity=3)
|
baker.make("clients.Site", client=cycle(clients), _quantity=3) # type: ignore
|
||||||
resp = self.client.get(url, format="json")
|
resp = self.client.get(url, format="json")
|
||||||
clients = Client.objects.all()
|
clients = Client.objects.all()
|
||||||
serializer = PolicyOverviewSerializer(clients, many=True)
|
serializer = PolicyOverviewSerializer(clients, many=True)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_get_related(self):
|
def test_get_related(self):
|
||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
url = f"/automation/policies/{policy.pk}/related/"
|
url = f"/automation/policies/{policy.pk}/related/" # type: ignore
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
resp = self.client.get(url, format="json")
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertIsInstance(resp.data["server_clients"], list)
|
self.assertIsInstance(resp.data["server_clients"], list) # type: ignore
|
||||||
self.assertIsInstance(resp.data["workstation_clients"], list)
|
self.assertIsInstance(resp.data["workstation_clients"], list) # type: ignore
|
||||||
self.assertIsInstance(resp.data["server_sites"], list)
|
self.assertIsInstance(resp.data["server_sites"], list) # type: ignore
|
||||||
self.assertIsInstance(resp.data["workstation_sites"], list)
|
self.assertIsInstance(resp.data["workstation_sites"], list) # type: ignore
|
||||||
self.assertIsInstance(resp.data["agents"], list)
|
self.assertIsInstance(resp.data["agents"], list) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -257,16 +258,16 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
|
|
||||||
# create policy managed tasks
|
# create policy managed tasks
|
||||||
policy_tasks = baker.make(
|
policy_tasks = baker.make(
|
||||||
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5
|
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"
|
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore
|
||||||
|
|
||||||
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
|
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
|
||||||
resp = self.client.patch(url, format="json")
|
resp = self.client.patch(url, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.assertEqual(len(resp.data), 5)
|
self.assertEqual(len(resp.data), 5) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
@@ -284,7 +285,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
resp = self.client.put(url, format="json")
|
resp = self.client.put(url, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
mock_task.assert_called_once_with([task.pk for task in tasks])
|
mock_task.assert_called_once_with([task.pk for task in tasks]) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"critical": "approve",
|
"critical": "approve",
|
||||||
"important": "approve",
|
"important": "approve",
|
||||||
"moderate": "ignore",
|
"moderate": "ignore",
|
||||||
@@ -325,11 +326,11 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
|
|
||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
|
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
|
||||||
url = f"/automation/winupdatepolicy/{patch_policy.pk}/"
|
url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"id": patch_policy.pk,
|
"id": patch_policy.pk, # type: ignore
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"critical": "approve",
|
"critical": "approve",
|
||||||
"important": "approve",
|
"important": "approve",
|
||||||
"moderate": "ignore",
|
"moderate": "ignore",
|
||||||
@@ -358,10 +359,10 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
clients = baker.make("clients.Client", _quantity=6)
|
clients = baker.make("clients.Client", _quantity=6)
|
||||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
|
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10) # type: ignore
|
||||||
agents = baker.make_recipe(
|
agents = baker.make_recipe(
|
||||||
"agents.agent",
|
"agents.agent",
|
||||||
site=cycle(sites),
|
site=cycle(sites), # type: ignore
|
||||||
_quantity=6,
|
_quantity=6,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -371,24 +372,24 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# test reset agents in site
|
# test reset agents in site
|
||||||
data = {"site": sites[0].id}
|
data = {"site": sites[0].id} # type: ignore
|
||||||
|
|
||||||
resp = self.client.patch(url, data, format="json")
|
resp = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
agents = Agent.objects.filter(site=sites[0])
|
agents = Agent.objects.filter(site=sites[0]) # type: ignore
|
||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
for k, v in inherit_fields.items():
|
for k, v in inherit_fields.items():
|
||||||
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
|
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
|
||||||
|
|
||||||
# test reset agents in client
|
# test reset agents in client
|
||||||
data = {"client": clients[1].id}
|
data = {"client": clients[1].id} # type: ignore
|
||||||
|
|
||||||
resp = self.client.patch(url, data, format="json")
|
resp = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
agents = Agent.objects.filter(site__client=clients[1])
|
agents = Agent.objects.filter(site__client=clients[1]) # type: ignore
|
||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
for k, v in inherit_fields.items():
|
for k, v in inherit_fields.items():
|
||||||
@@ -425,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("delete", url)
|
self.check_not_authenticated("delete", url)
|
||||||
|
|
||||||
|
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||||
|
def test_sync_policy(self, generate_checks):
|
||||||
|
url = "/automation/sync/"
|
||||||
|
|
||||||
|
# test invalid data
|
||||||
|
data = {"invalid": 7}
|
||||||
|
|
||||||
|
resp = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
|
||||||
|
policy = baker.make("automation.Policy", active=True)
|
||||||
|
data = {"policy": policy.pk} # type: ignore
|
||||||
|
|
||||||
|
resp = self.client.post(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyTasks(TacticalTestCase):
|
class TestPolicyTasks(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -435,46 +455,46 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
|
|
||||||
# Get Site and Client from an agent in list
|
# Get Site and Client from an agent in list
|
||||||
clients = baker.make("clients.Client", _quantity=5)
|
clients = baker.make("clients.Client", _quantity=5)
|
||||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
|
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25) # type: ignore
|
||||||
server_agents = baker.make_recipe(
|
server_agents = baker.make_recipe(
|
||||||
"agents.server_agent",
|
"agents.server_agent",
|
||||||
site=cycle(sites),
|
site=cycle(sites), # type: ignore
|
||||||
_quantity=25,
|
_quantity=25,
|
||||||
)
|
)
|
||||||
workstation_agents = baker.make_recipe(
|
workstation_agents = baker.make_recipe(
|
||||||
"agents.workstation_agent",
|
"agents.workstation_agent",
|
||||||
site=cycle(sites),
|
site=cycle(sites), # type: ignore
|
||||||
_quantity=25,
|
_quantity=25,
|
||||||
)
|
)
|
||||||
|
|
||||||
policy = baker.make("automation.Policy", active=True)
|
policy = baker.make("automation.Policy", active=True)
|
||||||
|
|
||||||
# Add Client to Policy
|
# Add Client to Policy
|
||||||
policy.server_clients.add(server_agents[13].client)
|
policy.server_clients.add(server_agents[13].client) # type: ignore
|
||||||
policy.workstation_clients.add(workstation_agents[15].client)
|
policy.workstation_clients.add(workstation_agents[15].client) # type: ignore
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/automation/policies/{policy.pk}/related/", format="json"
|
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEquals(len(resp.data["server_clients"]), 1)
|
self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore
|
||||||
self.assertEquals(len(resp.data["server_sites"]), 5)
|
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
|
||||||
self.assertEquals(len(resp.data["workstation_clients"]), 1)
|
self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore
|
||||||
self.assertEquals(len(resp.data["workstation_sites"]), 5)
|
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
|
||||||
self.assertEquals(len(resp.data["agents"]), 10)
|
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||||
|
|
||||||
# Add Site to Policy and the agents and sites length shouldn't change
|
# Add Site to Policy and the agents and sites length shouldn't change
|
||||||
policy.server_sites.add(server_agents[13].site)
|
policy.server_sites.add(server_agents[13].site) # type: ignore
|
||||||
policy.workstation_sites.add(workstation_agents[15].site)
|
policy.workstation_sites.add(workstation_agents[15].site) # type: ignore
|
||||||
self.assertEquals(len(resp.data["server_sites"]), 5)
|
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
|
||||||
self.assertEquals(len(resp.data["workstation_sites"]), 5)
|
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
|
||||||
self.assertEquals(len(resp.data["agents"]), 10)
|
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||||
|
|
||||||
# Add Agent to Policy and the agents length shouldn't change
|
# Add Agent to Policy and the agents length shouldn't change
|
||||||
policy.agents.add(server_agents[13])
|
policy.agents.add(server_agents[13]) # type: ignore
|
||||||
policy.agents.add(workstation_agents[15])
|
policy.agents.add(workstation_agents[15]) # type: ignore
|
||||||
self.assertEquals(len(resp.data["agents"]), 10)
|
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||||
|
|
||||||
def test_generating_agent_policy_checks(self):
|
def test_generating_agent_policy_checks(self):
|
||||||
from .tasks import generate_agent_checks_from_policies_task
|
from .tasks import generate_agent_checks_from_policies_task
|
||||||
@@ -485,7 +505,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
agent = baker.make_recipe("agents.agent", policy=policy)
|
agent = baker.make_recipe("agents.agent", policy=policy)
|
||||||
|
|
||||||
# test policy assigned to agent
|
# test policy assigned to agent
|
||||||
generate_agent_checks_from_policies_task(policy.id)
|
generate_agent_checks_from_policies_task(policy.id) # type: ignore
|
||||||
|
|
||||||
# make sure all checks were created. should be 7
|
# make sure all checks were created. should be 7
|
||||||
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
|
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
|
||||||
@@ -505,12 +525,12 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
self.assertEqual(check.ip, checks[1].ip)
|
self.assertEqual(check.ip, checks[1].ip)
|
||||||
elif check.check_type == "cpuload":
|
elif check.check_type == "cpuload":
|
||||||
self.assertEqual(check.parent_check, checks[2].id)
|
self.assertEqual(check.parent_check, checks[2].id)
|
||||||
self.assertEqual(check.error_threshold, checks[0].error_threshold)
|
self.assertEqual(check.error_threshold, checks[2].error_threshold)
|
||||||
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
|
self.assertEqual(check.warning_threshold, checks[2].warning_threshold)
|
||||||
elif check.check_type == "memory":
|
elif check.check_type == "memory":
|
||||||
self.assertEqual(check.parent_check, checks[3].id)
|
self.assertEqual(check.parent_check, checks[3].id)
|
||||||
self.assertEqual(check.error_threshold, checks[0].error_threshold)
|
self.assertEqual(check.error_threshold, checks[3].error_threshold)
|
||||||
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
|
self.assertEqual(check.warning_threshold, checks[3].warning_threshold)
|
||||||
elif check.check_type == "winsvc":
|
elif check.check_type == "winsvc":
|
||||||
self.assertEqual(check.parent_check, checks[4].id)
|
self.assertEqual(check.parent_check, checks[4].id)
|
||||||
self.assertEqual(check.svc_name, checks[4].svc_name)
|
self.assertEqual(check.svc_name, checks[4].svc_name)
|
||||||
@@ -535,7 +555,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
|
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
|
||||||
self.create_checks(agent=agent, script=script)
|
self.create_checks(agent=agent, script=script)
|
||||||
|
|
||||||
generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
|
generate_agent_checks_from_policies_task(policy.id, create_tasks=True) # type: ignore
|
||||||
|
|
||||||
# make sure each agent check says overriden_by_policy
|
# make sure each agent check says overriden_by_policy
|
||||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
|
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
|
||||||
@@ -843,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||||
|
|
||||||
# pick a policy check and delete it from the agent
|
# pick a policy check and delete it from the agent
|
||||||
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id
|
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
|
||||||
|
|
||||||
delete_policy_check_task(policy_check_id)
|
delete_policy_check_task(policy_check_id)
|
||||||
|
|
||||||
@@ -868,7 +888,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
|
|
||||||
# pick a policy check and update it with new values
|
# pick a policy check and update it with new values
|
||||||
ping_check = (
|
ping_check = (
|
||||||
Policy.objects.get(pk=policy.id)
|
Policy.objects.get(pk=policy.id) # type: ignore
|
||||||
.policychecks.filter(check_type="ping")
|
.policychecks.filter(check_type="ping")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -895,7 +915,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||||
|
|
||||||
generate_agent_tasks_from_policies_task(policy.id)
|
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
|
||||||
|
|
||||||
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
|
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
|
||||||
|
|
||||||
@@ -905,14 +925,14 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
for task in agent_tasks:
|
for task in agent_tasks:
|
||||||
self.assertTrue(task.managed_by_policy)
|
self.assertTrue(task.managed_by_policy)
|
||||||
if task.name == "Task1":
|
if task.name == "Task1":
|
||||||
self.assertEqual(task.parent_task, tasks[0].id)
|
self.assertEqual(task.parent_task, tasks[0].id) # type: ignore
|
||||||
self.assertEqual(task.name, tasks[0].name)
|
self.assertEqual(task.name, tasks[0].name) # type: ignore
|
||||||
if task.name == "Task2":
|
if task.name == "Task2":
|
||||||
self.assertEqual(task.parent_task, tasks[1].id)
|
self.assertEqual(task.parent_task, tasks[1].id) # type: ignore
|
||||||
self.assertEqual(task.name, tasks[1].name)
|
self.assertEqual(task.name, tasks[1].name) # type: ignore
|
||||||
if task.name == "Task3":
|
if task.name == "Task3":
|
||||||
self.assertEqual(task.parent_task, tasks[2].id)
|
self.assertEqual(task.parent_task, tasks[2].id) # type: ignore
|
||||||
self.assertEqual(task.name, tasks[2].name)
|
self.assertEqual(task.name, tasks[2].name) # type: ignore
|
||||||
|
|
||||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||||
def test_delete_policy_tasks(self, delete_win_task_schedule):
|
def test_delete_policy_tasks(self, delete_win_task_schedule):
|
||||||
@@ -922,10 +942,10 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||||
|
|
||||||
delete_policy_autotask_task(tasks[0].id)
|
delete_policy_autotask_task(tasks[0].id) # type: ignore
|
||||||
|
|
||||||
delete_win_task_schedule.assert_called_with(
|
delete_win_task_schedule.assert_called_with(
|
||||||
agent.autotasks.get(parent_task=tasks[0].id).id
|
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("autotasks.tasks.run_win_task.delay")
|
@patch("autotasks.tasks.run_win_task.delay")
|
||||||
@@ -934,12 +954,12 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
|
|
||||||
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
|
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
|
||||||
|
|
||||||
run_win_policy_autotask_task([task.id for task in tasks])
|
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
|
||||||
|
|
||||||
run_win_task.side_effect = [task.id for task in tasks]
|
run_win_task.side_effect = [task.id for task in tasks] # type: ignore
|
||||||
self.assertEqual(run_win_task.call_count, 3)
|
self.assertEqual(run_win_task.call_count, 3)
|
||||||
for task in tasks:
|
for task in tasks: # type: ignore
|
||||||
run_win_task.assert_any_call(task.id)
|
run_win_task.assert_any_call(task.id) # type: ignore
|
||||||
|
|
||||||
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
|
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
|
||||||
def test_update_policy_tasks(self, enable_or_disable_win_task):
|
def test_update_policy_tasks(self, enable_or_disable_win_task):
|
||||||
@@ -952,17 +972,17 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||||
|
|
||||||
tasks[0].enabled = False
|
tasks[0].enabled = False # type: ignore
|
||||||
tasks[0].save()
|
tasks[0].save() # type: ignore
|
||||||
|
|
||||||
update_policy_task_fields_task(tasks[0].id)
|
update_policy_task_fields_task(tasks[0].id) # type: ignore
|
||||||
enable_or_disable_win_task.assert_not_called()
|
enable_or_disable_win_task.assert_not_called()
|
||||||
|
|
||||||
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
|
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
|
||||||
|
|
||||||
update_policy_task_fields_task(tasks[0].id, update_agent=True)
|
update_policy_task_fields_task(tasks[0].id, update_agent=True) # type: ignore
|
||||||
enable_or_disable_win_task.assert_called_with(
|
enable_or_disable_win_task.assert_called_with(
|
||||||
agent.autotasks.get(parent_task=tasks[0].id).id, False
|
agent.autotasks.get(parent_task=tasks[0].id).id, False # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("agents.models.Agent.generate_tasks_from_policies")
|
@patch("agents.models.Agent.generate_tasks_from_policies")
|
||||||
@@ -984,3 +1004,110 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
|
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)
|
||||||
self.assertEquals(generate_checks.call_count, 5)
|
self.assertEquals(generate_checks.call_count, 5)
|
||||||
|
|
||||||
|
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||||
|
def test_policy_exclusions(self, delete_task):
|
||||||
|
# setup data
|
||||||
|
policy = baker.make("automation.Policy", active=True)
|
||||||
|
baker.make_recipe("checks.memory_check", policy=policy)
|
||||||
|
task = baker.make("autotasks.AutomatedTask", policy=policy)
|
||||||
|
agent = baker.make_recipe(
|
||||||
|
"agents.agent", policy=policy, monitoring_type="server"
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure related agents on policy returns correctly
|
||||||
|
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
|
||||||
|
|
||||||
|
# add agent to policy exclusions
|
||||||
|
policy.excluded_agents.set([agent]) # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||||
|
delete_task.assert_called()
|
||||||
|
delete_task.reset_mock()
|
||||||
|
|
||||||
|
# delete agent tasks
|
||||||
|
agent.autotasks.all().delete()
|
||||||
|
policy.excluded_agents.clear() # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
# make sure related agents on policy returns correctly
|
||||||
|
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
|
||||||
|
|
||||||
|
# add policy exclusions to site
|
||||||
|
policy.excluded_sites.set([agent.site]) # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||||
|
delete_task.assert_called()
|
||||||
|
delete_task.reset_mock()
|
||||||
|
|
||||||
|
# delete agent tasks and reset
|
||||||
|
agent.autotasks.all().delete()
|
||||||
|
policy.excluded_sites.clear() # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
# make sure related agents on policy returns correctly
|
||||||
|
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
|
||||||
|
|
||||||
|
# add policy exclusions to client
|
||||||
|
policy.excluded_clients.set([agent.client]) # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||||
|
delete_task.assert_called()
|
||||||
|
delete_task.reset_mock()
|
||||||
|
|
||||||
|
# delete agent tasks and reset
|
||||||
|
agent.autotasks.all().delete()
|
||||||
|
policy.excluded_clients.clear() # type: ignore
|
||||||
|
agent.policy = None
|
||||||
|
agent.save()
|
||||||
|
|
||||||
|
# test on default policy
|
||||||
|
core = CoreSettings.objects.first()
|
||||||
|
core.server_policy = policy
|
||||||
|
core.save()
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
# make sure related agents on policy returns correctly
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
|
||||||
|
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
|
||||||
|
|
||||||
|
# add policy exclusions to client
|
||||||
|
policy.excluded_clients.set([agent.client]) # type: ignore
|
||||||
|
|
||||||
|
agent.generate_checks_from_policies()
|
||||||
|
agent.generate_tasks_from_policies()
|
||||||
|
|
||||||
|
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||||
|
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||||
|
delete_task.assert_called()
|
||||||
|
delete_task.reset_mock()
|
||||||
|
|
||||||
|
def test_removing_duplicate_pending_task_actions(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_creating_checks_with_assigned_tasks(self):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
|||||||
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
|
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
|
||||||
path("policies/overview/", views.OverviewPolicy.as_view()),
|
path("policies/overview/", views.OverviewPolicy.as_view()),
|
||||||
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
|
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
|
||||||
|
path("sync/", views.PolicySync.as_view()),
|
||||||
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
|
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
|
||||||
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
|
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
|
||||||
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
|
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from autotasks.models import AutomatedTask
|
|||||||
from checks.models import Check
|
from checks.models import Check
|
||||||
from clients.models import Client
|
from clients.models import Client
|
||||||
from clients.serializers import ClientSerializer, SiteSerializer
|
from clients.serializers import ClientSerializer, SiteSerializer
|
||||||
|
from tacticalrmm.utils import notify_error
|
||||||
from winupdate.models import WinUpdatePolicy
|
from winupdate.models import WinUpdatePolicy
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
|
|
||||||
@@ -72,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
|
|||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class PolicySync(APIView):
|
||||||
|
def post(self, request):
|
||||||
|
if "policy" in request.data.keys():
|
||||||
|
from automation.tasks import generate_agent_checks_from_policies_task
|
||||||
|
|
||||||
|
generate_agent_checks_from_policies_task.delay(
|
||||||
|
request.data["policy"], create_tasks=True
|
||||||
|
)
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return notify_error("The request was invalid")
|
||||||
|
|
||||||
|
|
||||||
class PolicyAutoTask(APIView):
|
class PolicyAutoTask(APIView):
|
||||||
|
|
||||||
# tasks associated with policy
|
# tasks associated with policy
|
||||||
@@ -171,7 +186,7 @@ class UpdatePatchPolicy(APIView):
|
|||||||
|
|
||||||
serializer = WinUpdatePolicySerializer(data=request.data, partial=True)
|
serializer = WinUpdatePolicySerializer(data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.policy = policy
|
serializer.policy = policy # type: ignore
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-02-24 05:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('autotasks', '0017_auto_20210210_1512'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='automatedtask',
|
||||||
|
name='run_asap_after_missed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,7 +7,6 @@ from django.conf import settings
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields import DateTimeField
|
from django.db.models.fields import DateTimeField
|
||||||
from django.utils import timezone as djangotime
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from alerts.models import SEVERITY_CHOICES
|
from alerts.models import SEVERITY_CHOICES
|
||||||
@@ -96,6 +95,7 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
)
|
)
|
||||||
run_time_date = DateTimeField(null=True, blank=True)
|
run_time_date = DateTimeField(null=True, blank=True)
|
||||||
remove_if_not_scheduled = models.BooleanField(default=False)
|
remove_if_not_scheduled = models.BooleanField(default=False)
|
||||||
|
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
|
||||||
managed_by_policy = models.BooleanField(default=False)
|
managed_by_policy = models.BooleanField(default=False)
|
||||||
parent_task = models.PositiveIntegerField(null=True, blank=True)
|
parent_task = models.PositiveIntegerField(null=True, blank=True)
|
||||||
win_task_name = models.CharField(max_length=255, null=True, blank=True)
|
win_task_name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
@@ -218,90 +218,13 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
enabled=self.enabled,
|
enabled=self.enabled,
|
||||||
remove_if_not_scheduled=self.remove_if_not_scheduled,
|
remove_if_not_scheduled=self.remove_if_not_scheduled,
|
||||||
|
run_asap_after_missed=self.run_asap_after_missed,
|
||||||
)
|
)
|
||||||
|
|
||||||
create_win_task_schedule.delay(task.pk)
|
create_win_task_schedule.delay(task.pk)
|
||||||
|
|
||||||
def handle_alert(self) -> None:
|
def should_create_alert(self, alert_template=None):
|
||||||
from alerts.models import Alert
|
return (
|
||||||
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():
|
|
||||||
|
|
||||||
# check if alert should be created and if not return
|
|
||||||
if (
|
|
||||||
self.dashboard_alert
|
self.dashboard_alert
|
||||||
or self.email_alert
|
or self.email_alert
|
||||||
or self.text_alert
|
or self.text_alert
|
||||||
@@ -313,86 +236,12 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
or alert_template.task_always_text
|
or alert_template.task_always_text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
|
||||||
alert = Alert.create_task_alert(self)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
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):
|
def send_email(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
|
|
||||||
if self.agent:
|
if self.agent:
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||||
@@ -404,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
CORE.send_mail(subject, body, alert_template)
|
CORE.send_mail(subject, body, self.agent.alert_template)
|
||||||
|
|
||||||
def send_sms(self):
|
def send_sms(self):
|
||||||
|
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
|
|
||||||
if self.agent:
|
if self.agent:
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||||
@@ -423,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
CORE.send_sms(body, alert_template=alert_template)
|
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
def send_resolved_email(self):
|
def send_resolved_email(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||||
body = (
|
body = (
|
||||||
@@ -437,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
|
|||||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
def send_resolved_sms(self):
|
def send_resolved_sms(self):
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
|
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||||
body = (
|
body = (
|
||||||
subject
|
subject
|
||||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||||
)
|
)
|
||||||
CORE.send_sms(body, alert_template=alert_template)
|
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||||||
def get_alert_template(self, obj):
|
def get_alert_template(self, obj):
|
||||||
|
|
||||||
if obj.agent:
|
if obj.agent:
|
||||||
alert_template = obj.agent.get_alert_template()
|
alert_template = obj.agent.alert_template
|
||||||
else:
|
else:
|
||||||
alert_template = None
|
alert_template = None
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def create_win_task_schedule(pk, pending_action=False):
|
|||||||
task.run_time_date = now.astimezone(agent_tz).replace(
|
task.run_time_date = now.astimezone(agent_tz).replace(
|
||||||
tzinfo=pytz.utc
|
tzinfo=pytz.utc
|
||||||
) + djangotime.timedelta(minutes=5)
|
) + djangotime.timedelta(minutes=5)
|
||||||
task.save()
|
task.save(update_fields=["run_time_date"])
|
||||||
|
|
||||||
nats_data = {
|
nats_data = {
|
||||||
"func": "schedtask",
|
"func": "schedtask",
|
||||||
@@ -62,9 +62,12 @@ def create_win_task_schedule(pk, pending_action=False):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.remove_if_not_scheduled and pyver.parse(
|
if task.run_asap_after_missed and pyver.parse(
|
||||||
task.agent.version
|
task.agent.version
|
||||||
) >= pyver.parse("1.1.2"):
|
) >= pyver.parse("1.4.7"):
|
||||||
|
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
|
||||||
|
|
||||||
|
if task.remove_if_not_scheduled:
|
||||||
nats_data["schedtaskpayload"]["deleteafter"] = True
|
nats_data["schedtaskpayload"]["deleteafter"] = True
|
||||||
|
|
||||||
elif task.task_type == "checkfailure" or task.task_type == "manual":
|
elif task.task_type == "checkfailure" or task.task_type == "manual":
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class TestAutotaskViews(TacticalTestCase):
|
|||||||
agent = baker.make_recipe("agents.agent")
|
agent = baker.make_recipe("agents.agent")
|
||||||
policy = baker.make("automation.Policy")
|
policy = baker.make("automation.Policy")
|
||||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||||
old_agent = baker.make_recipe("agents.agent", version="1.1.0")
|
|
||||||
|
|
||||||
# test script set to invalid pk
|
# test script set to invalid pk
|
||||||
data = {"autotask": {"script": 500}}
|
data = {"autotask": {"script": 500}}
|
||||||
@@ -52,15 +51,6 @@ class TestAutotaskViews(TacticalTestCase):
|
|||||||
resp = self.client.post(url, data, format="json")
|
resp = self.client.post(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 404)
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
# test old agent version
|
|
||||||
data = {
|
|
||||||
"autotask": {"script": script.id},
|
|
||||||
"agent": old_agent.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = self.client.post(url, data, format="json")
|
|
||||||
self.assertEqual(resp.status_code, 400)
|
|
||||||
|
|
||||||
# test add task to agent
|
# test add task to agent
|
||||||
data = {
|
data = {
|
||||||
"autotask": {
|
"autotask": {
|
||||||
@@ -203,13 +193,6 @@ class TestAutotaskViews(TacticalTestCase):
|
|||||||
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
|
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
|
||||||
nats_cmd.reset_mock()
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
old_agent = baker.make_recipe("agents.agent", version="1.0.2")
|
|
||||||
task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
|
|
||||||
url = f"/tasks/runwintask/{task2.id}/"
|
|
||||||
resp = self.client.get(url, format="json")
|
|
||||||
self.assertEqual(resp.status_code, 400)
|
|
||||||
nats_cmd.assert_not_called()
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ class AddAutoTask(APIView):
|
|||||||
parent = {"policy": policy}
|
parent = {"policy": policy}
|
||||||
else:
|
else:
|
||||||
agent = get_object_or_404(Agent, pk=data["agent"])
|
agent = get_object_or_404(Agent, pk=data["agent"])
|
||||||
if not agent.has_gotasks:
|
|
||||||
return notify_error("Requires agent version 1.1.1 or greater")
|
|
||||||
|
|
||||||
parent = {"agent": agent}
|
parent = {"agent": agent}
|
||||||
|
|
||||||
check = None
|
check = None
|
||||||
@@ -128,8 +125,5 @@ class AutoTask(APIView):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def run_task(request, pk):
|
def run_task(request, pk):
|
||||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||||
if not task.agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
||||||
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from model_bakery.recipe import Recipe
|
|||||||
check = Recipe("checks.Check")
|
check = Recipe("checks.Check")
|
||||||
|
|
||||||
diskspace_check = check.extend(
|
diskspace_check = check.extend(
|
||||||
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
|
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10
|
||||||
)
|
)
|
||||||
|
|
||||||
cpuload_check = check.extend(
|
cpuload_check = check.extend(
|
||||||
@@ -13,7 +13,7 @@ cpuload_check = check.extend(
|
|||||||
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
|
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
|
||||||
|
|
||||||
memory_check = check.extend(
|
memory_check = check.extend(
|
||||||
check_type="memory", warning_threshold=30, error_threshold=75
|
check_type="memory", warning_threshold=60, error_threshold=75
|
||||||
)
|
)
|
||||||
|
|
||||||
winsvc_check = check.extend(
|
winsvc_check = check.extend(
|
||||||
@@ -21,6 +21,7 @@ winsvc_check = check.extend(
|
|||||||
svc_name="ServiceName",
|
svc_name="ServiceName",
|
||||||
svc_display_name="ServiceName",
|
svc_display_name="ServiceName",
|
||||||
svc_policy_mode="manual",
|
svc_policy_mode="manual",
|
||||||
|
pass_if_svc_not_exist=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
eventlog_check = check.extend(
|
eventlog_check = check.extend(
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-06 02:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('checks', '0021_auto_20210212_1429'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='check',
|
||||||
|
name='number_of_events_b4_alert',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=1, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
api/tacticalrmm/checks/migrations/0023_check_run_interval.py
Normal file
18
api/tacticalrmm/checks/migrations/0023_check_run_interval.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-06 02:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('checks', '0022_check_number_of_events_b4_alert'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='check',
|
||||||
|
name='run_interval',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,27 +3,19 @@ import json
|
|||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Any, List, Union
|
from typing import Any
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone as djangotime
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from rest_framework.fields import JSONField
|
|
||||||
|
|
||||||
from alerts.models import SEVERITY_CHOICES
|
from alerts.models import SEVERITY_CHOICES
|
||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
from logs.models import BaseAuditModel
|
from logs.models import BaseAuditModel
|
||||||
|
|
||||||
from .tasks import (
|
|
||||||
handle_check_email_alert_task,
|
|
||||||
handle_check_sms_alert_task,
|
|
||||||
handle_resolved_check_email_alert_task,
|
|
||||||
handle_resolved_check_sms_alert_task,
|
|
||||||
)
|
|
||||||
from .utils import bytes2human
|
from .utils import bytes2human
|
||||||
|
|
||||||
logger.configure(**settings.LOG_CONFIG)
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
@@ -101,6 +93,7 @@ class Check(BaseAuditModel):
|
|||||||
fail_count = models.PositiveIntegerField(default=0)
|
fail_count = models.PositiveIntegerField(default=0)
|
||||||
outage_history = models.JSONField(null=True, blank=True) # store
|
outage_history = models.JSONField(null=True, blank=True) # store
|
||||||
extra_details = models.JSONField(null=True, blank=True)
|
extra_details = models.JSONField(null=True, blank=True)
|
||||||
|
run_interval = models.PositiveIntegerField(blank=True, default=0)
|
||||||
# check specific fields
|
# check specific fields
|
||||||
|
|
||||||
# for eventlog, script, ip, and service alert severity
|
# for eventlog, script, ip, and service alert severity
|
||||||
@@ -189,6 +182,9 @@ class Check(BaseAuditModel):
|
|||||||
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
|
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
|
||||||
)
|
)
|
||||||
search_last_days = models.PositiveIntegerField(null=True, blank=True)
|
search_last_days = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
number_of_events_b4_alert = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True, default=1
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.agent:
|
if self.agent:
|
||||||
@@ -206,9 +202,9 @@ class Check(BaseAuditModel):
|
|||||||
if self.error_threshold:
|
if self.error_threshold:
|
||||||
text += f" Error Threshold: {self.error_threshold}%"
|
text += f" Error Threshold: {self.error_threshold}%"
|
||||||
|
|
||||||
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
|
return f"{self.get_check_type_display()}: Drive {self.disk} - {text}" # type: ignore
|
||||||
elif self.check_type == "ping":
|
elif self.check_type == "ping":
|
||||||
return f"{self.get_check_type_display()}: {self.name}"
|
return f"{self.get_check_type_display()}: {self.name}" # type: ignore
|
||||||
elif self.check_type == "cpuload" or self.check_type == "memory":
|
elif self.check_type == "cpuload" or self.check_type == "memory":
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
@@ -217,13 +213,13 @@ class Check(BaseAuditModel):
|
|||||||
if self.error_threshold:
|
if self.error_threshold:
|
||||||
text += f" Error Threshold: {self.error_threshold}%"
|
text += f" Error Threshold: {self.error_threshold}%"
|
||||||
|
|
||||||
return f"{self.get_check_type_display()} > {text}"
|
return f"{self.get_check_type_display()} - {text}" # type: ignore
|
||||||
elif self.check_type == "winsvc":
|
elif self.check_type == "winsvc":
|
||||||
return f"{self.get_check_type_display()}: {self.svc_display_name}"
|
return f"{self.get_check_type_display()}: {self.svc_display_name}" # type: ignore
|
||||||
elif self.check_type == "eventlog":
|
elif self.check_type == "eventlog":
|
||||||
return f"{self.get_check_type_display()}: {self.name}"
|
return f"{self.get_check_type_display()}: {self.name}" # type: ignore
|
||||||
elif self.check_type == "script":
|
elif self.check_type == "script":
|
||||||
return f"{self.get_check_type_display()}: {self.script.name}"
|
return f"{self.get_check_type_display()}: {self.script.name}" # type: ignore
|
||||||
else:
|
else:
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
@@ -242,7 +238,7 @@ class Check(BaseAuditModel):
|
|||||||
return self.last_run
|
return self.last_run
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def non_editable_fields(self) -> List[str]:
|
def non_editable_fields(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
"check_type",
|
"check_type",
|
||||||
"status",
|
"status",
|
||||||
@@ -267,73 +263,9 @@ class Check(BaseAuditModel):
|
|||||||
"modified_time",
|
"modified_time",
|
||||||
]
|
]
|
||||||
|
|
||||||
def handle_alert(self) -> None:
|
def should_create_alert(self, alert_template=None):
|
||||||
from alerts.models import Alert, AlertTemplate
|
|
||||||
|
|
||||||
# return if agent is in maintenance mode
|
return (
|
||||||
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():
|
|
||||||
|
|
||||||
# check if alert should be created and if not return
|
|
||||||
if (
|
|
||||||
self.dashboard_alert
|
self.dashboard_alert
|
||||||
or self.email_alert
|
or self.email_alert
|
||||||
or self.text_alert
|
or self.text_alert
|
||||||
@@ -345,86 +277,13 @@ class Check(BaseAuditModel):
|
|||||||
or alert_template.check_always_text
|
or alert_template.check_always_text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
|
||||||
alert = Alert.create_check_alert(self)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
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:
|
def add_check_history(self, value: int, more_info: Any = None) -> None:
|
||||||
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
|
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
|
||||||
|
|
||||||
def handle_checkv2(self, data):
|
def handle_checkv2(self, data):
|
||||||
|
from alerts.models import Alert
|
||||||
|
|
||||||
# cpuload or mem checks
|
# cpuload or mem checks
|
||||||
if self.check_type == "cpuload" or self.check_type == "memory":
|
if self.check_type == "cpuload" or self.check_type == "memory":
|
||||||
@@ -633,13 +492,13 @@ class Check(BaseAuditModel):
|
|||||||
log.append(i)
|
log.append(i)
|
||||||
|
|
||||||
if self.fail_when == "contains":
|
if self.fail_when == "contains":
|
||||||
if log:
|
if log and len(log) >= self.number_of_events_b4_alert:
|
||||||
self.status = "failing"
|
self.status = "failing"
|
||||||
else:
|
else:
|
||||||
self.status = "passing"
|
self.status = "passing"
|
||||||
|
|
||||||
elif self.fail_when == "not_contains":
|
elif self.fail_when == "not_contains":
|
||||||
if log:
|
if log and len(log) >= self.number_of_events_b4_alert:
|
||||||
self.status = "passing"
|
self.status = "passing"
|
||||||
else:
|
else:
|
||||||
self.status = "failing"
|
self.status = "failing"
|
||||||
@@ -657,11 +516,14 @@ class Check(BaseAuditModel):
|
|||||||
self.fail_count += 1
|
self.fail_count += 1
|
||||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||||
|
|
||||||
|
if self.fail_count >= self.fails_b4_alert:
|
||||||
|
Alert.handle_alert_failure(self)
|
||||||
|
|
||||||
elif self.status == "passing":
|
elif self.status == "passing":
|
||||||
self.fail_count = 0
|
self.fail_count = 0
|
||||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||||
|
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
|
||||||
self.handle_alert()
|
Alert.handle_alert_resolve(self)
|
||||||
|
|
||||||
return self.status
|
return self.status
|
||||||
|
|
||||||
@@ -705,6 +567,7 @@ class Check(BaseAuditModel):
|
|||||||
text_alert=self.text_alert,
|
text_alert=self.text_alert,
|
||||||
fails_b4_alert=self.fails_b4_alert,
|
fails_b4_alert=self.fails_b4_alert,
|
||||||
extra_details=self.extra_details,
|
extra_details=self.extra_details,
|
||||||
|
run_interval=self.run_interval,
|
||||||
error_threshold=self.error_threshold,
|
error_threshold=self.error_threshold,
|
||||||
warning_threshold=self.warning_threshold,
|
warning_threshold=self.warning_threshold,
|
||||||
disk=self.disk,
|
disk=self.disk,
|
||||||
@@ -728,6 +591,7 @@ class Check(BaseAuditModel):
|
|||||||
event_message=self.event_message,
|
event_message=self.event_message,
|
||||||
fail_when=self.fail_when,
|
fail_when=self.fail_when,
|
||||||
search_last_days=self.search_last_days,
|
search_last_days=self.search_last_days,
|
||||||
|
number_of_events_b4_alert=self.number_of_events_b4_alert,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_duplicate(self, check):
|
def is_duplicate(self, check):
|
||||||
@@ -755,11 +619,10 @@ class Check(BaseAuditModel):
|
|||||||
def send_email(self):
|
def send_email(self):
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
|
|
||||||
body: str = ""
|
body: str = ""
|
||||||
if self.agent:
|
if self.agent:
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
|
||||||
else:
|
else:
|
||||||
subject = f"{self} Failed"
|
subject = f"{self} Failed"
|
||||||
|
|
||||||
@@ -837,12 +700,11 @@ class Check(BaseAuditModel):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
def send_sms(self):
|
def send_sms(self):
|
||||||
|
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
body: str = ""
|
body: str = ""
|
||||||
|
|
||||||
if self.agent:
|
if self.agent:
|
||||||
@@ -886,21 +748,21 @@ class Check(BaseAuditModel):
|
|||||||
elif self.check_type == "eventlog":
|
elif self.check_type == "eventlog":
|
||||||
body = subject
|
body = subject
|
||||||
|
|
||||||
CORE.send_sms(body, alert_template=alert_template)
|
CORE.send_sms(body, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
def send_resolved_email(self):
|
def send_resolved_email(self):
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||||
body = f"{self} is now back to normal"
|
body = f"{self} is now back to normal"
|
||||||
|
|
||||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
def send_resolved_sms(self):
|
def send_resolved_sms(self):
|
||||||
CORE = CoreSettings.objects.first()
|
CORE = CoreSettings.objects.first()
|
||||||
alert_template = self.agent.get_alert_template()
|
|
||||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||||
CORE.send_sms(subject, alert_template=alert_template)
|
CORE.send_sms(subject, alert_template=self.agent.alert_template)
|
||||||
|
|
||||||
|
|
||||||
class CheckHistory(models.Model):
|
class CheckHistory(models.Model):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_alert_template(self, obj):
|
def get_alert_template(self, obj):
|
||||||
if obj.agent:
|
if obj.agent:
|
||||||
alert_template = obj.agent.get_alert_template()
|
alert_template = obj.agent.alert_template
|
||||||
else:
|
else:
|
||||||
alert_template = None
|
alert_template = None
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
serializer = CheckSerializer(disk_check)
|
serializer = CheckSerializer(disk_check)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_add_disk_check(self):
|
def test_add_disk_check(self):
|
||||||
@@ -211,7 +211,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
serializer = CheckSerializer(disk_check)
|
serializer = CheckSerializer(disk_check)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data, serializer.data)
|
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
def test_add_policy_disk_check(self):
|
def test_add_policy_disk_check(self):
|
||||||
@@ -221,7 +221,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
url = "/checks/checks/"
|
url = "/checks/checks/"
|
||||||
|
|
||||||
valid_payload = {
|
valid_payload = {
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"check": {
|
"check": {
|
||||||
"check_type": "diskspace",
|
"check_type": "diskspace",
|
||||||
"disk": "M:",
|
"disk": "M:",
|
||||||
@@ -233,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
|
|
||||||
# should fail because both error and warning thresholds are 0
|
# should fail because both error and warning thresholds are 0
|
||||||
invalid_payload = {
|
invalid_payload = {
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"check": {
|
"check": {
|
||||||
"check_type": "diskspace",
|
"check_type": "diskspace",
|
||||||
"error_threshold": 0,
|
"error_threshold": 0,
|
||||||
@@ -247,7 +247,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
|
|
||||||
# should fail because warning is less than error
|
# should fail because warning is less than error
|
||||||
invalid_payload = {
|
invalid_payload = {
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"check": {
|
"check": {
|
||||||
"check_type": "diskspace",
|
"check_type": "diskspace",
|
||||||
"error_threshold": 80,
|
"error_threshold": 80,
|
||||||
@@ -261,7 +261,7 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
|
|
||||||
# this should fail because we already have a check for drive M: in setup
|
# this should fail because we already have a check for drive M: in setup
|
||||||
invalid_payload = {
|
invalid_payload = {
|
||||||
"policy": policy.pk,
|
"policy": policy.pk, # type: ignore
|
||||||
"check": {
|
"check": {
|
||||||
"check_type": "diskspace",
|
"check_type": "diskspace",
|
||||||
"disk": "M:",
|
"disk": "M:",
|
||||||
@@ -277,8 +277,8 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
def test_get_disks_for_policies(self):
|
def test_get_disks_for_policies(self):
|
||||||
url = "/checks/getalldisks/"
|
url = "/checks/getalldisks/"
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertIsInstance(r.data, list)
|
self.assertIsInstance(r.data, list) # type: ignore
|
||||||
self.assertEqual(26, len(r.data))
|
self.assertEqual(26, len(r.data)) # type: ignore
|
||||||
|
|
||||||
def test_edit_check_alert(self):
|
def test_edit_check_alert(self):
|
||||||
# setup data
|
# setup data
|
||||||
@@ -310,14 +310,8 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
@patch("agents.models.Agent.nats_cmd")
|
@patch("agents.models.Agent.nats_cmd")
|
||||||
def test_run_checks(self, nats_cmd):
|
def test_run_checks(self, nats_cmd):
|
||||||
agent = baker.make_recipe("agents.agent", version="1.4.1")
|
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")
|
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}/"
|
url = f"/checks/runchecks/{agent_b4_141.pk}/"
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
@@ -361,8 +355,8 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# need to manually set the date back 35 days
|
# need to manually set the date back 35 days
|
||||||
for check_history in check_history_data:
|
for check_history in check_history_data: # type: ignore
|
||||||
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
|
check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore
|
||||||
check_history.save()
|
check_history.save()
|
||||||
|
|
||||||
# test invalid check pk
|
# test invalid check pk
|
||||||
@@ -375,20 +369,22 @@ class TestCheckViews(TacticalTestCase):
|
|||||||
data = {"timeFilter": 30}
|
data = {"timeFilter": 30}
|
||||||
resp = self.client.patch(url, data, format="json")
|
resp = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(len(resp.data), 30)
|
self.assertEqual(len(resp.data), 30) # type: ignore
|
||||||
|
|
||||||
# test with timeFilter equal to 0
|
# test with timeFilter equal to 0
|
||||||
data = {"timeFilter": 0}
|
data = {"timeFilter": 0}
|
||||||
resp = self.client.patch(url, data, format="json")
|
resp = self.client.patch(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(len(resp.data), 60)
|
self.assertEqual(len(resp.data), 60) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("patch", url)
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
|
|
||||||
class TestCheckTasks(TacticalTestCase):
|
class TestCheckTasks(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.authenticate()
|
||||||
self.setup_coresettings()
|
self.setup_coresettings()
|
||||||
|
self.agent = baker.make_recipe("agents.agent")
|
||||||
|
|
||||||
def test_prune_check_history(self):
|
def test_prune_check_history(self):
|
||||||
from .tasks import prune_check_history
|
from .tasks import prune_check_history
|
||||||
@@ -403,8 +399,8 @@ class TestCheckTasks(TacticalTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# need to manually set the date back 35 days
|
# need to manually set the date back 35 days
|
||||||
for check_history in check_history_data:
|
for check_history in check_history_data: # type: ignore
|
||||||
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
|
check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore
|
||||||
check_history.save()
|
check_history.save()
|
||||||
|
|
||||||
# prune data 30 days old
|
# prune data 30 days old
|
||||||
@@ -414,3 +410,758 @@ class TestCheckTasks(TacticalTestCase):
|
|||||||
# prune all Check history Data
|
# prune all Check history Data
|
||||||
prune_check_history(0)
|
prune_check_history(0)
|
||||||
self.assertEqual(CheckHistory.objects.count(), 0)
|
self.assertEqual(CheckHistory.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_handle_script_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
script = baker.make_recipe("checks.script_check", agent=self.agent)
|
||||||
|
|
||||||
|
# test failing
|
||||||
|
data = {
|
||||||
|
"id": script.id,
|
||||||
|
"retcode": 500,
|
||||||
|
"stderr": "error",
|
||||||
|
"stdout": "message",
|
||||||
|
"runtime": 5.000,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=script.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing
|
||||||
|
data = {
|
||||||
|
"id": script.id,
|
||||||
|
"retcode": 0,
|
||||||
|
"stderr": "error",
|
||||||
|
"stdout": "message",
|
||||||
|
"runtime": 5.000,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=script.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test failing info
|
||||||
|
script.info_return_codes = [20, 30, 50]
|
||||||
|
script.save()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": script.id,
|
||||||
|
"retcode": 30,
|
||||||
|
"stderr": "error",
|
||||||
|
"stdout": "message",
|
||||||
|
"runtime": 5.000,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=script.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "info")
|
||||||
|
|
||||||
|
# test failing warning
|
||||||
|
script.warning_return_codes = [80, 100, 1040]
|
||||||
|
script.save()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": script.id,
|
||||||
|
"retcode": 1040,
|
||||||
|
"stderr": "error",
|
||||||
|
"stdout": "message",
|
||||||
|
"runtime": 5.000,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=script.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
def test_handle_diskspace_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
diskspace = baker.make_recipe(
|
||||||
|
"checks.diskspace_check",
|
||||||
|
warning_threshold=20,
|
||||||
|
error_threshold=10,
|
||||||
|
agent=self.agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test warning threshold failure
|
||||||
|
data = {
|
||||||
|
"id": diskspace.id,
|
||||||
|
"exists": True,
|
||||||
|
"percent_used": 85,
|
||||||
|
"total": 500,
|
||||||
|
"free": 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
# test error failure
|
||||||
|
data = {
|
||||||
|
"id": diskspace.id,
|
||||||
|
"exists": True,
|
||||||
|
"percent_used": 95,
|
||||||
|
"total": 500,
|
||||||
|
"free": 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test disk not exist
|
||||||
|
data = {"id": diskspace.id, "exists": False}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test warning threshold 0
|
||||||
|
diskspace.warning_threshold = 0
|
||||||
|
diskspace.save()
|
||||||
|
data = {
|
||||||
|
"id": diskspace.id,
|
||||||
|
"exists": True,
|
||||||
|
"percent_used": 95,
|
||||||
|
"total": 500,
|
||||||
|
"free": 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test error threshold 0
|
||||||
|
diskspace.warning_threshold = 50
|
||||||
|
diskspace.error_threshold = 0
|
||||||
|
diskspace.save()
|
||||||
|
data = {
|
||||||
|
"id": diskspace.id,
|
||||||
|
"exists": True,
|
||||||
|
"percent_used": 95,
|
||||||
|
"total": 500,
|
||||||
|
"free": 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
# test passing
|
||||||
|
data = {
|
||||||
|
"id": diskspace.id,
|
||||||
|
"exists": True,
|
||||||
|
"percent_used": 50,
|
||||||
|
"total": 500,
|
||||||
|
"free": 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=diskspace.id)
|
||||||
|
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
def test_handle_cpuload_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
cpuload = baker.make_recipe(
|
||||||
|
"checks.cpuload_check",
|
||||||
|
warning_threshold=70,
|
||||||
|
error_threshold=90,
|
||||||
|
agent=self.agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test failing warning
|
||||||
|
data = {"id": cpuload.id, "percent": 80}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=cpuload.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
# test failing error
|
||||||
|
data = {"id": cpuload.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
cpuload.history = []
|
||||||
|
cpuload.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=cpuload.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing
|
||||||
|
data = {"id": cpuload.id, "percent": 50}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
cpuload.history = []
|
||||||
|
cpuload.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=cpuload.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test warning threshold 0
|
||||||
|
cpuload.warning_threshold = 0
|
||||||
|
cpuload.save()
|
||||||
|
data = {"id": cpuload.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
cpuload.history = []
|
||||||
|
cpuload.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=cpuload.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test error threshold 0
|
||||||
|
cpuload.warning_threshold = 50
|
||||||
|
cpuload.error_threshold = 0
|
||||||
|
cpuload.save()
|
||||||
|
data = {"id": cpuload.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
cpuload.history = []
|
||||||
|
cpuload.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=cpuload.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
def test_handle_memory_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
memory = baker.make_recipe(
|
||||||
|
"checks.memory_check",
|
||||||
|
warning_threshold=70,
|
||||||
|
error_threshold=90,
|
||||||
|
agent=self.agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test failing warning
|
||||||
|
data = {"id": memory.id, "percent": 80}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=memory.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
# test failing error
|
||||||
|
data = {"id": memory.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
memory.history = []
|
||||||
|
memory.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=memory.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing
|
||||||
|
data = {"id": memory.id, "percent": 50}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
memory.history = []
|
||||||
|
memory.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=memory.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test warning threshold 0
|
||||||
|
memory.warning_threshold = 0
|
||||||
|
memory.save()
|
||||||
|
data = {"id": memory.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
memory.history = []
|
||||||
|
memory.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=memory.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test error threshold 0
|
||||||
|
memory.warning_threshold = 50
|
||||||
|
memory.error_threshold = 0
|
||||||
|
memory.save()
|
||||||
|
data = {"id": memory.id, "percent": 95}
|
||||||
|
|
||||||
|
# reset check history
|
||||||
|
memory.history = []
|
||||||
|
memory.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=memory.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
def test_handle_ping_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
ping = baker.make_recipe(
|
||||||
|
"checks.ping_check", agent=self.agent, alert_severity="info"
|
||||||
|
)
|
||||||
|
|
||||||
|
# test failing info
|
||||||
|
data = {
|
||||||
|
"id": ping.id,
|
||||||
|
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||||
|
"has_stdout": True,
|
||||||
|
"has_stderr": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=ping.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "info")
|
||||||
|
|
||||||
|
# test failing warning
|
||||||
|
data = {
|
||||||
|
"id": ping.id,
|
||||||
|
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||||
|
"has_stdout": True,
|
||||||
|
"has_stderr": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
ping.alert_severity = "warning"
|
||||||
|
ping.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=ping.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
|
||||||
|
# test failing error
|
||||||
|
data = {
|
||||||
|
"id": ping.id,
|
||||||
|
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||||
|
"has_stdout": True,
|
||||||
|
"has_stderr": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
ping.alert_severity = "error"
|
||||||
|
ping.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=ping.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test failing error
|
||||||
|
data = {
|
||||||
|
"id": ping.id,
|
||||||
|
"output": "some output",
|
||||||
|
"has_stdout": False,
|
||||||
|
"has_stderr": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=ping.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing
|
||||||
|
data = {
|
||||||
|
"id": ping.id,
|
||||||
|
"output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
|
||||||
|
"has_stdout": True,
|
||||||
|
"has_stderr": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=ping.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
@patch("agents.models.Agent.nats_cmd")
|
||||||
|
def test_handle_winsvc_check(self, nats_cmd):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
winsvc = baker.make_recipe(
|
||||||
|
"checks.winsvc_check", agent=self.agent, alert_severity="info"
|
||||||
|
)
|
||||||
|
|
||||||
|
# test passing running
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "running"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test passing start pending
|
||||||
|
winsvc.pass_if_start_pending = True
|
||||||
|
winsvc.save()
|
||||||
|
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test failing no start
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "info")
|
||||||
|
|
||||||
|
# test failing and attempt start
|
||||||
|
winsvc.restart_if_stopped = True
|
||||||
|
winsvc.alert_severity = "warning"
|
||||||
|
winsvc.save()
|
||||||
|
|
||||||
|
nats_cmd.return_value = "timeout"
|
||||||
|
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "warning")
|
||||||
|
nats_cmd.assert_called()
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
|
# test failing and attempt start
|
||||||
|
winsvc.alert_severity = "error"
|
||||||
|
winsvc.save()
|
||||||
|
nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
|
||||||
|
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
self.assertEqual(new_check.alert_severity, "error")
|
||||||
|
nats_cmd.assert_called()
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
|
# test success and attempt start
|
||||||
|
nats_cmd.return_value = {"success": True}
|
||||||
|
|
||||||
|
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
nats_cmd.assert_called()
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
|
# test failing and service not exist
|
||||||
|
data = {"id": winsvc.id, "exists": False, "status": ""}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "failing")
|
||||||
|
|
||||||
|
# test success and service not exist
|
||||||
|
winsvc.pass_if_svc_not_exist = True
|
||||||
|
winsvc.save()
|
||||||
|
data = {"id": winsvc.id, "exists": False, "status": ""}
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=winsvc.id)
|
||||||
|
self.assertEqual(new_check.status, "passing")
|
||||||
|
|
||||||
|
def test_handle_eventlog_check(self):
|
||||||
|
from checks.models import Check
|
||||||
|
|
||||||
|
url = "/api/v3/checkrunner/"
|
||||||
|
|
||||||
|
eventlog = baker.make_recipe(
|
||||||
|
"checks.eventlog_check",
|
||||||
|
event_type="warning",
|
||||||
|
fail_when="contains",
|
||||||
|
event_id=123,
|
||||||
|
alert_severity="warning",
|
||||||
|
agent=self.agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": eventlog.id,
|
||||||
|
"log": [
|
||||||
|
{
|
||||||
|
"eventType": "warning",
|
||||||
|
"eventID": 150,
|
||||||
|
"source": "source",
|
||||||
|
"message": "a test message",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "warning",
|
||||||
|
"eventID": 123,
|
||||||
|
"source": "source",
|
||||||
|
"message": "a test message",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "error",
|
||||||
|
"eventID": 123,
|
||||||
|
"source": "source",
|
||||||
|
"message": "a test message",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "error",
|
||||||
|
"eventID": 123,
|
||||||
|
"source": "source",
|
||||||
|
"message": "a test message",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# test failing when contains
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.alert_severity, "warning")
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
|
||||||
|
# test passing when not contains and message
|
||||||
|
eventlog.event_message = "doesnt exist"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test failing when not contains and message and source
|
||||||
|
eventlog.fail_when = "not_contains"
|
||||||
|
eventlog.alert_severity = "error"
|
||||||
|
eventlog.event_message = "doesnt exist"
|
||||||
|
eventlog.event_source = "doesnt exist"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
self.assertEquals(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing when contains with source and message
|
||||||
|
eventlog.event_message = "test"
|
||||||
|
eventlog.event_source = "source"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test failing with wildcard not contains and source
|
||||||
|
eventlog.event_id_is_wildcard = True
|
||||||
|
eventlog.event_source = "doesn't exist"
|
||||||
|
eventlog.event_message = ""
|
||||||
|
eventlog.event_id = 0
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
self.assertEquals(new_check.alert_severity, "error")
|
||||||
|
|
||||||
|
# test passing with wildcard contains
|
||||||
|
eventlog.event_source = ""
|
||||||
|
eventlog.event_message = ""
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test failing with wildcard contains and message
|
||||||
|
eventlog.fail_when = "contains"
|
||||||
|
eventlog.event_type = "error"
|
||||||
|
eventlog.alert_severity = "info"
|
||||||
|
eventlog.event_message = "test"
|
||||||
|
eventlog.event_source = ""
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
self.assertEquals(new_check.alert_severity, "info")
|
||||||
|
|
||||||
|
# test passing with wildcard not contains message and source
|
||||||
|
eventlog.event_message = "doesnt exist"
|
||||||
|
eventlog.event_source = "doesnt exist"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|
||||||
|
# test multiple events found and contains
|
||||||
|
# this should pass since only two events are found
|
||||||
|
eventlog.number_of_events_b4_alert = 3
|
||||||
|
eventlog.event_id_is_wildcard = False
|
||||||
|
eventlog.event_source = None
|
||||||
|
eventlog.event_message = None
|
||||||
|
eventlog.event_id = 123
|
||||||
|
eventlog.event_type = "error"
|
||||||
|
eventlog.fail_when = "contains"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|
||||||
|
# this should pass since there are two events returned
|
||||||
|
eventlog.number_of_events_b4_alert = 2
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
|
||||||
|
# test not contains
|
||||||
|
# this should fail since only two events are found
|
||||||
|
eventlog.number_of_events_b4_alert = 3
|
||||||
|
eventlog.event_id_is_wildcard = False
|
||||||
|
eventlog.event_source = None
|
||||||
|
eventlog.event_message = None
|
||||||
|
eventlog.event_id = 123
|
||||||
|
eventlog.event_type = "error"
|
||||||
|
eventlog.fail_when = "not_contains"
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "failing")
|
||||||
|
|
||||||
|
# this should pass since there are two events returned
|
||||||
|
eventlog.number_of_events_b4_alert = 2
|
||||||
|
eventlog.save()
|
||||||
|
|
||||||
|
resp = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
new_check = Check.objects.get(pk=eventlog.id)
|
||||||
|
|
||||||
|
self.assertEquals(new_check.status, "passing")
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class AddCheck(APIView):
|
|||||||
if policy:
|
if policy:
|
||||||
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
|
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
|
||||||
elif agent:
|
elif agent:
|
||||||
checks = agent.agentchecks.filter(
|
checks = agent.agentchecks.filter( # type: ignore
|
||||||
check_type=obj.check_type, managed_by_policy=True
|
check_type=obj.check_type, managed_by_policy=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ class CheckHistory(APIView):
|
|||||||
- djangotime.timedelta(days=request.data["timeFilter"]),
|
- djangotime.timedelta(days=request.data["timeFilter"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
check_history = check.check_history.filter(timeFilter).order_by("-x")
|
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
CheckHistorySerializer(
|
CheckHistorySerializer(
|
||||||
@@ -161,8 +161,6 @@ class CheckHistory(APIView):
|
|||||||
@api_view()
|
@api_view()
|
||||||
def run_checks(request, pk):
|
def run_checks(request, pk):
|
||||||
agent = get_object_or_404(Agent, pk=pk)
|
agent = get_object_or_404(Agent, pk=pk)
|
||||||
if not agent.has_nats:
|
|
||||||
return notify_error("Requires agent version 1.1.0 or greater")
|
|
||||||
|
|
||||||
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
|
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
|
||||||
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
|
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Client, Deployment, Site
|
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||||
|
|
||||||
admin.site.register(Client)
|
admin.site.register(Client)
|
||||||
admin.site.register(Site)
|
admin.site.register(Site)
|
||||||
admin.site.register(Deployment)
|
admin.site.register(Deployment)
|
||||||
|
admin.site.register(ClientCustomField)
|
||||||
|
admin.site.register(SiteCustomField)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_customfield'),
|
||||||
|
('clients', '0009_auto_20210212_1408'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SiteCustomField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('value', models.TextField(blank=True, null=True)),
|
||||||
|
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientCustomField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('value', models.TextField(blank=True, null=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
|
||||||
|
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-21 15:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0010_clientcustomfield_sitecustomfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='site',
|
||||||
|
unique_together={('client', 'name')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-26 06:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0011_auto_20210321_1511'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deployment',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 02:51
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0012_deployment_created'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clientcustomfield',
|
||||||
|
name='multiple_value',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sitecustomfield',
|
||||||
|
name='multiple_value',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 03:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0013_auto_20210329_0251'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clientcustomfield',
|
||||||
|
name='checkbox_value',
|
||||||
|
field=models.BooleanField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sitecustomfield',
|
||||||
|
name='checkbox_value',
|
||||||
|
field=models.BooleanField(blank=True, default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0014_auto_20210329_0301'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='clientcustomfield',
|
||||||
|
old_name='checkbox_value',
|
||||||
|
new_name='bool_value',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='clientcustomfield',
|
||||||
|
old_name='value',
|
||||||
|
new_name='string_value',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='sitecustomfield',
|
||||||
|
name='value',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 18:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('clients', '0015_auto_20210329_1709'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='sitecustomfield',
|
||||||
|
old_name='checkbox_value',
|
||||||
|
new_name='bool_value',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sitecustomfield',
|
||||||
|
name='string_value',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from agents.models import Agent
|
from agents.models import Agent
|
||||||
@@ -32,6 +33,7 @@ class Client(BaseAuditModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kw):
|
def save(self, *args, **kw):
|
||||||
|
from alerts.tasks import cache_agents_alert_template
|
||||||
from automation.tasks import generate_agent_checks_by_location_task
|
from automation.tasks import generate_agent_checks_by_location_task
|
||||||
|
|
||||||
# get old client if exists
|
# get old client if exists
|
||||||
@@ -54,6 +56,9 @@ class Client(BaseAuditModel):
|
|||||||
create_tasks=True,
|
create_tasks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if old_client and old_client.alert_template != self.alert_template:
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|
||||||
@@ -127,6 +132,7 @@ class Site(BaseAuditModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kw):
|
def save(self, *args, **kw):
|
||||||
|
from alerts.tasks import cache_agents_alert_template
|
||||||
from automation.tasks import generate_agent_checks_by_location_task
|
from automation.tasks import generate_agent_checks_by_location_task
|
||||||
|
|
||||||
# get old client if exists
|
# get old client if exists
|
||||||
@@ -149,8 +155,12 @@ class Site(BaseAuditModel):
|
|||||||
create_tasks=True,
|
create_tasks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if old_site and old_site.alert_template != self.alert_template:
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
unique_together = (("client", "name"),)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -217,6 +227,7 @@ class Deployment(models.Model):
|
|||||||
)
|
)
|
||||||
arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
|
arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
|
||||||
expiry = models.DateTimeField(null=True, blank=True)
|
expiry = models.DateTimeField(null=True, blank=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||||
auth_token = models.ForeignKey(
|
auth_token = models.ForeignKey(
|
||||||
"knox.AuthToken", related_name="deploytokens", on_delete=models.CASCADE
|
"knox.AuthToken", related_name="deploytokens", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
@@ -225,3 +236,73 @@ class Deployment(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.client} - {self.site} - {self.mon_type}"
|
return f"{self.client} - {self.site} - {self.mon_type}"
|
||||||
|
|
||||||
|
|
||||||
|
class ClientCustomField(models.Model):
|
||||||
|
client = models.ForeignKey(
|
||||||
|
Client,
|
||||||
|
related_name="custom_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
field = models.ForeignKey(
|
||||||
|
"core.CustomField",
|
||||||
|
related_name="client_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
string_value = models.TextField(null=True, blank=True)
|
||||||
|
bool_value = models.BooleanField(blank=True, default=False)
|
||||||
|
multiple_value = ArrayField(
|
||||||
|
models.TextField(null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.field.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self.field.type == "multiple":
|
||||||
|
return self.multiple_value
|
||||||
|
elif self.field.type == "checkbox":
|
||||||
|
return self.bool_value
|
||||||
|
else:
|
||||||
|
return self.string_value
|
||||||
|
|
||||||
|
|
||||||
|
class SiteCustomField(models.Model):
|
||||||
|
site = models.ForeignKey(
|
||||||
|
Site,
|
||||||
|
related_name="custom_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
field = models.ForeignKey(
|
||||||
|
"core.CustomField",
|
||||||
|
related_name="site_fields",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
string_value = models.TextField(null=True, blank=True)
|
||||||
|
bool_value = models.BooleanField(blank=True, default=False)
|
||||||
|
multiple_value = ArrayField(
|
||||||
|
models.TextField(null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.field.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self.field.type == "multiple":
|
||||||
|
return self.multiple_value
|
||||||
|
elif self.field.type == "checkbox":
|
||||||
|
return self.bool_value
|
||||||
|
else:
|
||||||
|
return self.string_value
|
||||||
|
|||||||
@@ -1,42 +1,87 @@
|
|||||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
|
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
|
||||||
|
|
||||||
from .models import Client, Deployment, Site
|
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||||
|
|
||||||
|
|
||||||
|
class SiteCustomFieldSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SiteCustomField
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"field",
|
||||||
|
"site",
|
||||||
|
"value",
|
||||||
|
"string_value",
|
||||||
|
"bool_value",
|
||||||
|
"multiple_value",
|
||||||
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
"string_value": {"write_only": True},
|
||||||
|
"bool_value": {"write_only": True},
|
||||||
|
"multiple_value": {"write_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteSerializer(ModelSerializer):
|
class SiteSerializer(ModelSerializer):
|
||||||
client_name = ReadOnlyField(source="client.name")
|
client_name = ReadOnlyField(source="client.name")
|
||||||
|
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = "__all__"
|
fields = (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"server_policy",
|
||||||
|
"workstation_policy",
|
||||||
|
"alert_template",
|
||||||
|
"client_name",
|
||||||
|
"client",
|
||||||
|
"custom_fields",
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, val):
|
def validate(self, val):
|
||||||
if "name" in val.keys() and "|" in val["name"]:
|
if "name" in val.keys() and "|" in val["name"]:
|
||||||
raise ValidationError("Site name cannot contain the | character")
|
raise ValidationError("Site name cannot contain the | character")
|
||||||
|
|
||||||
if self.context:
|
|
||||||
client = Client.objects.get(pk=self.context["clientpk"])
|
|
||||||
if Site.objects.filter(client=client, name=val["name"]).exists():
|
|
||||||
raise ValidationError(f"Site {val['name']} already exists")
|
|
||||||
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
class ClientCustomFieldSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ClientCustomField
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"field",
|
||||||
|
"client",
|
||||||
|
"value",
|
||||||
|
"string_value",
|
||||||
|
"bool_value",
|
||||||
|
"multiple_value",
|
||||||
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
"string_value": {"write_only": True},
|
||||||
|
"bool_value": {"write_only": True},
|
||||||
|
"multiple_value": {"write_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ClientSerializer(ModelSerializer):
|
class ClientSerializer(ModelSerializer):
|
||||||
sites = SiteSerializer(many=True, read_only=True)
|
sites = SiteSerializer(many=True, read_only=True)
|
||||||
|
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Client
|
model = Client
|
||||||
fields = "__all__"
|
fields = (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"server_policy",
|
||||||
|
"workstation_policy",
|
||||||
|
"alert_template",
|
||||||
|
"sites",
|
||||||
|
"custom_fields",
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, val):
|
def validate(self, val):
|
||||||
|
|
||||||
if "site" in self.context:
|
|
||||||
if "|" in self.context["site"]:
|
|
||||||
raise ValidationError("Site name cannot contain the | character")
|
|
||||||
if len(self.context["site"]) > 255:
|
|
||||||
raise ValidationError("Site name too long")
|
|
||||||
|
|
||||||
if "name" in val.keys() and "|" in val["name"]:
|
if "name" in val.keys() and "|" in val["name"]:
|
||||||
raise ValidationError("Client name cannot contain the | character")
|
raise ValidationError("Client name cannot contain the | character")
|
||||||
|
|
||||||
@@ -83,4 +128,5 @@ class DeploymentSerializer(ModelSerializer):
|
|||||||
"arch",
|
"arch",
|
||||||
"expiry",
|
"expiry",
|
||||||
"install_flags",
|
"install_flags",
|
||||||
|
"created",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
from .models import Client, Deployment, Site
|
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
ClientSerializer,
|
ClientSerializer,
|
||||||
ClientTreeSerializer,
|
ClientTreeSerializer,
|
||||||
@@ -28,18 +29,29 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.get(url, format="json")
|
r = self.client.get(url, format="json")
|
||||||
serializer = ClientSerializer(clients, many=True)
|
serializer = ClientSerializer(clients, many=True)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, serializer.data)
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_add_client(self):
|
def test_add_client(self):
|
||||||
url = "/clients/clients/"
|
url = "/clients/clients/"
|
||||||
payload = {"client": "Company 1", "site": "Site 1"}
|
|
||||||
|
# test successfull add client
|
||||||
|
payload = {
|
||||||
|
"client": {"name": "Client1"},
|
||||||
|
"site": {"name": "Site1"},
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
payload["client"] = "Company1|askd"
|
# test add client with | in name
|
||||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
payload = {
|
||||||
|
"client": {"name": "Client2|d"},
|
||||||
|
"site": {"name": "Site1"},
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
|
serializer = ClientSerializer(data=payload["client"])
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
ValidationError, "Client name cannot contain the | character"
|
ValidationError, "Client name cannot contain the | character"
|
||||||
):
|
):
|
||||||
@@ -48,19 +60,22 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
payload = {"client": "Company 156", "site": "Site2|a34"}
|
# test add client with | in Site name
|
||||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
payload = {
|
||||||
with self.assertRaisesMessage(
|
"client": {"name": "Client2"},
|
||||||
ValidationError, "Site name cannot contain the | character"
|
"site": {"name": "Site1|fds"},
|
||||||
):
|
"custom_fields": [],
|
||||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
}
|
||||||
|
|
||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
# test unique
|
# test unique
|
||||||
payload = {"client": "Company 1", "site": "Site 1"}
|
payload = {
|
||||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
"client": {"name": "Client1"},
|
||||||
|
"site": {"name": "Site1"},
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
|
serializer = ClientSerializer(data=payload["client"])
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
ValidationError, "client with this name already exists."
|
ValidationError, "client with this name already exists."
|
||||||
):
|
):
|
||||||
@@ -69,67 +84,129 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
# test long site name
|
# test initial setup
|
||||||
payload = {"client": "Company 2394", "site": "Site123" * 100}
|
|
||||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
|
||||||
with self.assertRaisesMessage(ValidationError, "Site name too long"):
|
|
||||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
|
||||||
|
|
||||||
r = self.client.post(url, payload, format="json")
|
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"client": {"client": "Company 4", "site": "HQ"},
|
"client": {"name": "Setup Client"},
|
||||||
"initialsetup": True,
|
"site": {"name": "Setup Site"},
|
||||||
"timezone": "America/Los_Angeles",
|
"timezone": "America/Los_Angeles",
|
||||||
|
"initialsetup": True,
|
||||||
}
|
}
|
||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
# test add with custom fields
|
||||||
|
field = baker.make("core.CustomField", model="client", type="text")
|
||||||
|
payload = {
|
||||||
|
"client": {"name": "Custom Field Client"},
|
||||||
|
"site": {"name": "Setup Site"},
|
||||||
|
"custom_fields": [{"field": field.id, "string_value": "new Value"}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.post(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
client = Client.objects.get(name="Custom Field Client")
|
||||||
|
self.assertTrue(
|
||||||
|
ClientCustomField.objects.filter(client=client, field=field).exists()
|
||||||
|
)
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
def test_get_client(self):
|
||||||
|
# setup data
|
||||||
|
client = baker.make("clients.Client")
|
||||||
|
|
||||||
|
url = f"/clients/{client.id}/client/" # type: ignore
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
serializer = ClientSerializer(client)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_edit_client(self):
|
def test_edit_client(self):
|
||||||
# setup data
|
# setup data
|
||||||
client = baker.make("clients.Client")
|
client = baker.make("clients.Client", name="OldClientName")
|
||||||
|
|
||||||
# test invalid id
|
# test invalid id
|
||||||
r = self.client.put("/clients/500/client/", format="json")
|
r = self.client.put("/clients/500/client/", format="json")
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
data = {"id": client.id, "name": "New Name"}
|
# test successfull edit client
|
||||||
|
data = {"client": {"name": "NewClientName"}, "custom_fields": []}
|
||||||
url = f"/clients/{client.id}/client/"
|
url = f"/clients/{client.id}/client/" # type: ignore
|
||||||
r = self.client.put(url, data, format="json")
|
r = self.client.put(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertTrue(Client.objects.filter(name="New Name").exists())
|
self.assertTrue(Client.objects.filter(name="NewClientName").exists())
|
||||||
|
self.assertFalse(Client.objects.filter(name="OldClientName").exists())
|
||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
# test edit client with | in name
|
||||||
|
data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
|
||||||
def test_delete_client(self):
|
url = f"/clients/{client.id}/client/" # type: ignore
|
||||||
# setup data
|
r = self.client.put(url, data, format="json")
|
||||||
client = baker.make("clients.Client")
|
|
||||||
site = baker.make("clients.Site", client=client)
|
|
||||||
agent = baker.make_recipe("agents.agent", site=site)
|
|
||||||
|
|
||||||
# test invalid id
|
|
||||||
r = self.client.delete("/clients/500/client/", format="json")
|
|
||||||
self.assertEqual(r.status_code, 404)
|
|
||||||
|
|
||||||
url = f"/clients/{client.id}/client/"
|
|
||||||
|
|
||||||
# test deleting with agents under client
|
|
||||||
r = self.client.delete(url, format="json")
|
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
# test successful deletion
|
# test add with custom fields new value
|
||||||
agent.delete()
|
field = baker.make("core.CustomField", model="client", type="checkbox")
|
||||||
r = self.client.delete(url, format="json")
|
payload = {
|
||||||
|
"client": {
|
||||||
|
"id": client.id, # type: ignore
|
||||||
|
"name": "Custom Field Client",
|
||||||
|
},
|
||||||
|
"custom_fields": [{"field": field.id, "bool_value": True}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.put(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertFalse(Client.objects.filter(pk=client.id).exists())
|
|
||||||
self.assertFalse(Site.objects.filter(pk=site.id).exists())
|
client = Client.objects.get(name="Custom Field Client")
|
||||||
|
self.assertTrue(
|
||||||
|
ClientCustomField.objects.filter(client=client, field=field).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# edit custom field value
|
||||||
|
payload = {
|
||||||
|
"client": {
|
||||||
|
"id": client.id, # type: ignore
|
||||||
|
"name": "Custom Field Client",
|
||||||
|
},
|
||||||
|
"custom_fields": [{"field": field.id, "bool_value": False}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.put(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
ClientCustomField.objects.get(client=client, field=field).value
|
||||||
|
)
|
||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
|
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||||
|
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||||
|
def test_delete_client(self, task1, task2):
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
task1.return_value = "ok"
|
||||||
|
task2.return_value = "ok"
|
||||||
|
# setup data
|
||||||
|
client_to_delete = baker.make("clients.Client")
|
||||||
|
client_to_move = baker.make("clients.Client")
|
||||||
|
site_to_move = baker.make("clients.Site", client=client_to_move)
|
||||||
|
agent = baker.make_recipe("agents.agent", site=site_to_move)
|
||||||
|
|
||||||
|
# test invalid id
|
||||||
|
r = self.client.delete("/clients/334/953/", format="json")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
url = f"/clients/{client_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||||
|
|
||||||
|
# test successful deletion
|
||||||
|
r = self.client.delete(url, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
agent_moved = Agent.objects.get(pk=agent.pk)
|
||||||
|
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
|
||||||
|
self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists()) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("delete", url)
|
||||||
|
|
||||||
def test_get_sites(self):
|
def test_get_sites(self):
|
||||||
# setup data
|
# setup data
|
||||||
baker.make("clients.Site", _quantity=5)
|
baker.make("clients.Site", _quantity=5)
|
||||||
@@ -139,29 +216,31 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.get(url, format="json")
|
r = self.client.get(url, format="json")
|
||||||
serializer = SiteSerializer(sites, many=True)
|
serializer = SiteSerializer(sites, many=True)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, serializer.data)
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
def test_add_site(self):
|
def test_add_site(self):
|
||||||
# setup data
|
# setup data
|
||||||
site = baker.make("clients.Site")
|
client = baker.make("clients.Client")
|
||||||
|
site = baker.make("clients.Site", client=client)
|
||||||
|
|
||||||
url = "/clients/sites/"
|
url = "/clients/sites/"
|
||||||
|
|
||||||
# test success add
|
# test success add
|
||||||
payload = {"client": site.client.id, "name": "LA Office"}
|
payload = {
|
||||||
|
"site": {"client": client.id, "name": "LA Office"}, # type: ignore
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
r = self.client.post(url, payload, format="json")
|
r = self.client.post(url, payload, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertTrue(
|
|
||||||
Site.objects.filter(
|
|
||||||
name="LA Office", client__name=site.client.name
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
# test with | symbol
|
# test with | symbol
|
||||||
payload = {"client": site.client.id, "name": "LA Off|ice |*&@#$"}
|
payload = {
|
||||||
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
|
"site": {"client": client.id, "name": "LA Office |*&@#$"}, # type: ignore
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
|
serializer = SiteSerializer(data=payload["site"])
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
ValidationError, "Site name cannot contain the | character"
|
ValidationError, "Site name cannot contain the | character"
|
||||||
):
|
):
|
||||||
@@ -171,55 +250,139 @@ class TestClientViews(TacticalTestCase):
|
|||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
# test site already exists
|
# test site already exists
|
||||||
payload = {"client": site.client.id, "name": "LA Office"}
|
payload = {
|
||||||
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
|
"site": {"client": site.client.id, "name": "LA Office"}, # type: ignore
|
||||||
with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
|
"custom_fields": [],
|
||||||
|
}
|
||||||
|
serializer = SiteSerializer(data=payload["site"])
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
ValidationError, "The fields client, name must make a unique set."
|
||||||
|
):
|
||||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
self.assertFalse(serializer.is_valid(raise_exception=True))
|
||||||
|
|
||||||
|
# test add with custom fields
|
||||||
|
field = baker.make(
|
||||||
|
"core.CustomField",
|
||||||
|
model="site",
|
||||||
|
type="single",
|
||||||
|
options=["one", "two", "three"],
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"site": {"client": client.id, "name": "Custom Field Site"}, # type: ignore
|
||||||
|
"custom_fields": [{"field": field.id, "string_value": "one"}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.post(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
site = Site.objects.get(name="Custom Field Site")
|
||||||
|
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
def test_edit_site(self):
|
def test_get_site(self):
|
||||||
# setup data
|
# setup data
|
||||||
site = baker.make("clients.Site")
|
site = baker.make("clients.Site")
|
||||||
|
|
||||||
|
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||||
|
r = self.client.get(url, format="json")
|
||||||
|
serializer = SiteSerializer(site)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
def test_edit_site(self):
|
||||||
|
# setup data
|
||||||
|
client = baker.make("clients.Client")
|
||||||
|
site = baker.make("clients.Site", client=client)
|
||||||
|
|
||||||
# test invalid id
|
# test invalid id
|
||||||
r = self.client.put("/clients/500/site/", format="json")
|
r = self.client.put("/clients/sites/688/", format="json")
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
data = {"id": site.id, "name": "New Name", "client": site.client.id}
|
data = {
|
||||||
|
"site": {"client": client.id, "name": "New Site Name"}, # type: ignore
|
||||||
|
"custom_fields": [],
|
||||||
|
}
|
||||||
|
|
||||||
url = f"/clients/{site.id}/site/"
|
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||||
r = self.client.put(url, data, format="json")
|
r = self.client.put(url, data, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertTrue(Site.objects.filter(name="New Name").exists())
|
self.assertTrue(
|
||||||
|
Site.objects.filter(client=client, name="New Site Name").exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# test add with custom fields new value
|
||||||
|
field = baker.make(
|
||||||
|
"core.CustomField",
|
||||||
|
model="site",
|
||||||
|
type="multiple",
|
||||||
|
options=["one", "two", "three"],
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"site": {
|
||||||
|
"id": site.id, # type: ignore
|
||||||
|
"client": site.client.id, # type: ignore
|
||||||
|
"name": "Custom Field Site",
|
||||||
|
},
|
||||||
|
"custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.put(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
site = Site.objects.get(name="Custom Field Site")
|
||||||
|
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
|
||||||
|
|
||||||
|
# edit custom field value
|
||||||
|
payload = {
|
||||||
|
"site": {
|
||||||
|
"id": site.id, # type: ignore
|
||||||
|
"client": client.id, # type: ignore
|
||||||
|
"name": "Custom Field Site",
|
||||||
|
},
|
||||||
|
"custom_fields": [{"field": field.id, "multiple_value": ["one"]}], # type: ignore
|
||||||
|
}
|
||||||
|
r = self.client.put(url, payload, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
SiteCustomField.objects.get(site=site, field=field).value,
|
||||||
|
["one"],
|
||||||
|
)
|
||||||
|
|
||||||
self.check_not_authenticated("put", url)
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
def test_delete_site(self):
|
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||||
|
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||||
|
def test_delete_site(self, task1, task2):
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
task1.return_value = "ok"
|
||||||
|
task2.return_value = "ok"
|
||||||
# setup data
|
# setup data
|
||||||
site = baker.make("clients.Site")
|
client = baker.make("clients.Client")
|
||||||
agent = baker.make_recipe("agents.agent", site=site)
|
site_to_delete = baker.make("clients.Site", client=client)
|
||||||
|
site_to_move = baker.make("clients.Site")
|
||||||
|
agent = baker.make_recipe("agents.agent", site=site_to_delete)
|
||||||
|
|
||||||
# test invalid id
|
# test invalid id
|
||||||
r = self.client.delete("/clients/500/site/", format="json")
|
r = self.client.delete("/clients/500/445/", format="json")
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
url = f"/clients/{site.id}/site/"
|
url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||||
|
|
||||||
# test deleting with last site under client
|
# test deleting with last site under client
|
||||||
r = self.client.delete(url, format="json")
|
r = self.client.delete(url, format="json")
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
self.assertEqual(r.json(), "A client must have at least 1 site.")
|
||||||
# test deletion when agents exist under site
|
|
||||||
baker.make("clients.Site", client=site.client)
|
|
||||||
r = self.client.delete(url, format="json")
|
|
||||||
self.assertEqual(r.status_code, 400)
|
|
||||||
|
|
||||||
# test successful deletion
|
# test successful deletion
|
||||||
agent.delete()
|
site_to_move.client = client # type: ignore
|
||||||
|
site_to_move.save(update_fields=["client"]) # type: ignore
|
||||||
r = self.client.delete(url, format="json")
|
r = self.client.delete(url, format="json")
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertFalse(Site.objects.filter(pk=site.id).exists())
|
agent_moved = Agent.objects.get(pk=agent.pk)
|
||||||
|
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("delete", url)
|
self.check_not_authenticated("delete", url)
|
||||||
|
|
||||||
@@ -233,7 +396,7 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.get(url, format="json")
|
r = self.client.get(url, format="json")
|
||||||
serializer = ClientTreeSerializer(clients, many=True)
|
serializer = ClientTreeSerializer(clients, many=True)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, serializer.data)
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -245,7 +408,7 @@ class TestClientViews(TacticalTestCase):
|
|||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
serializer = DeploymentSerializer(deployments, many=True)
|
serializer = DeploymentSerializer(deployments, many=True)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(r.data, serializer.data)
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
@@ -255,8 +418,8 @@ class TestClientViews(TacticalTestCase):
|
|||||||
|
|
||||||
url = "/clients/deployments/"
|
url = "/clients/deployments/"
|
||||||
payload = {
|
payload = {
|
||||||
"client": site.client.id,
|
"client": site.client.id, # type: ignore
|
||||||
"site": site.id,
|
"site": site.id, # type: ignore
|
||||||
"expires": "2037-11-23 18:53",
|
"expires": "2037-11-23 18:53",
|
||||||
"power": 1,
|
"power": 1,
|
||||||
"ping": 0,
|
"ping": 0,
|
||||||
@@ -284,10 +447,10 @@ class TestClientViews(TacticalTestCase):
|
|||||||
|
|
||||||
url = "/clients/deployments/"
|
url = "/clients/deployments/"
|
||||||
|
|
||||||
url = f"/clients/{deployment.id}/deployment/"
|
url = f"/clients/{deployment.id}/deployment/" # type: ignore
|
||||||
r = self.client.delete(url)
|
r = self.client.delete(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
|
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) # type: ignore
|
||||||
|
|
||||||
url = "/clients/32348/deployment/"
|
url = "/clients/32348/deployment/"
|
||||||
r = self.client.delete(url)
|
r = self.client.delete(url)
|
||||||
@@ -301,7 +464,7 @@ class TestClientViews(TacticalTestCase):
|
|||||||
|
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
self.assertEqual(r.data, "invalid")
|
self.assertEqual(r.data, "invalid") # type: ignore
|
||||||
|
|
||||||
uid = uuid.uuid4()
|
uid = uuid.uuid4()
|
||||||
url = f"/clients/{uid}/deploy/"
|
url = f"/clients/{uid}/deploy/"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("clients/", views.GetAddClients.as_view()),
|
path("clients/", views.GetAddClients.as_view()),
|
||||||
path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
|
path("<int:pk>/client/", views.GetUpdateClient.as_view()),
|
||||||
|
path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
|
||||||
path("tree/", views.GetClientTree.as_view()),
|
path("tree/", views.GetClientTree.as_view()),
|
||||||
path("sites/", views.GetAddSites.as_view()),
|
path("sites/", views.GetAddSites.as_view()),
|
||||||
path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
|
path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
|
||||||
|
path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
|
||||||
path("deployments/", views.AgentDeployment.as_view()),
|
path("deployments/", views.AgentDeployment.as_view()),
|
||||||
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
|
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
|
||||||
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
|
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone as djangotime
|
from django.utils import timezone as djangotime
|
||||||
|
from loguru import logger
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -17,14 +15,18 @@ from agents.models import Agent
|
|||||||
from core.models import CoreSettings
|
from core.models import CoreSettings
|
||||||
from tacticalrmm.utils import notify_error
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .models import Client, Deployment, Site
|
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
|
ClientCustomFieldSerializer,
|
||||||
ClientSerializer,
|
ClientSerializer,
|
||||||
ClientTreeSerializer,
|
ClientTreeSerializer,
|
||||||
DeploymentSerializer,
|
DeploymentSerializer,
|
||||||
|
SiteCustomFieldSerializer,
|
||||||
SiteSerializer,
|
SiteSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.configure(**settings.LOG_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
class GetAddClients(APIView):
|
class GetAddClients(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@@ -32,44 +34,98 @@ class GetAddClients(APIView):
|
|||||||
return Response(ClientSerializer(clients, many=True).data)
|
return Response(ClientSerializer(clients, many=True).data)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
# create client
|
||||||
|
client_serializer = ClientSerializer(data=request.data["client"])
|
||||||
|
client_serializer.is_valid(raise_exception=True)
|
||||||
|
client = client_serializer.save()
|
||||||
|
|
||||||
if "initialsetup" in request.data:
|
# create site
|
||||||
client = {"name": request.data["client"]["client"].strip()}
|
site_serializer = SiteSerializer(
|
||||||
site = {"name": request.data["client"]["site"].strip()}
|
data={"client": client.id, "name": request.data["site"]["name"]}
|
||||||
serializer = ClientSerializer(data=client, context=request.data["client"])
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
# make sure site serializer doesn't return errors and save
|
||||||
|
if site_serializer.is_valid():
|
||||||
|
site_serializer.save()
|
||||||
|
else:
|
||||||
|
# delete client since site serializer was invalid
|
||||||
|
client.delete()
|
||||||
|
site_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
if "initialsetup" in request.data.keys():
|
||||||
core = CoreSettings.objects.first()
|
core = CoreSettings.objects.first()
|
||||||
core.default_time_zone = request.data["timezone"]
|
core.default_time_zone = request.data["timezone"]
|
||||||
core.save(update_fields=["default_time_zone"])
|
core.save(update_fields=["default_time_zone"])
|
||||||
else:
|
|
||||||
client = {"name": request.data["client"].strip()}
|
|
||||||
site = {"name": request.data["site"].strip()}
|
|
||||||
serializer = ClientSerializer(data=client, context=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
obj = serializer.save()
|
# save custom fields
|
||||||
Site(client=obj, name=site["name"]).save()
|
if "custom_fields" in request.data.keys():
|
||||||
|
for field in request.data["custom_fields"]:
|
||||||
|
|
||||||
return Response(f"{obj} was added!")
|
custom_field = field
|
||||||
|
custom_field["client"] = client.id
|
||||||
|
|
||||||
|
serializer = ClientCustomFieldSerializer(data=custom_field)
|
||||||
class GetUpdateDeleteClient(APIView):
|
|
||||||
def put(self, request, pk):
|
|
||||||
client = get_object_or_404(Client, pk=pk)
|
|
||||||
|
|
||||||
serializer = ClientSerializer(data=request.data, instance=client, partial=True)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response("The Client was renamed")
|
return Response(f"{client} was added!")
|
||||||
|
|
||||||
def delete(self, request, pk):
|
|
||||||
|
class GetUpdateClient(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
client = get_object_or_404(Client, pk=pk)
|
client = get_object_or_404(Client, pk=pk)
|
||||||
agent_count = Agent.objects.filter(site__client=client).count()
|
return Response(ClientSerializer(client).data)
|
||||||
if agent_count > 0:
|
|
||||||
return notify_error(
|
def put(self, request, pk):
|
||||||
f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
|
client = get_object_or_404(Client, pk=pk)
|
||||||
|
|
||||||
|
serializer = ClientSerializer(
|
||||||
|
data=request.data["client"], instance=client, partial=True
|
||||||
)
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# update custom fields
|
||||||
|
if "custom_fields" in request.data.keys():
|
||||||
|
for field in request.data["custom_fields"]:
|
||||||
|
|
||||||
|
custom_field = field
|
||||||
|
custom_field["client"] = pk
|
||||||
|
|
||||||
|
if ClientCustomField.objects.filter(field=field["field"], client=pk):
|
||||||
|
value = ClientCustomField.objects.get(
|
||||||
|
field=field["field"], client=pk
|
||||||
|
)
|
||||||
|
serializer = ClientCustomFieldSerializer(
|
||||||
|
instance=value, data=custom_field
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
else:
|
||||||
|
serializer = ClientCustomFieldSerializer(data=custom_field)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
return Response("The Client was updated")
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteClient(APIView):
|
||||||
|
def delete(self, request, pk, sitepk):
|
||||||
|
from automation.tasks import generate_all_agent_checks_task
|
||||||
|
|
||||||
|
client = get_object_or_404(Client, pk=pk)
|
||||||
|
agents = Agent.objects.filter(site__client=client)
|
||||||
|
|
||||||
|
if not sitepk:
|
||||||
|
return notify_error(
|
||||||
|
"There needs to be a site specified to move existing agents to"
|
||||||
|
)
|
||||||
|
|
||||||
|
site = get_object_or_404(Site, pk=sitepk)
|
||||||
|
agents.update(site=site)
|
||||||
|
|
||||||
|
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
|
||||||
|
generate_all_agent_checks_task.delay("server", create_tasks=True)
|
||||||
|
|
||||||
client.delete()
|
client.delete()
|
||||||
return Response(f"{client.name} was deleted!")
|
return Response(f"{client.name} was deleted!")
|
||||||
@@ -87,39 +143,90 @@ class GetAddSites(APIView):
|
|||||||
return Response(SiteSerializer(sites, many=True).data)
|
return Response(SiteSerializer(sites, many=True).data)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
name = request.data["name"].strip()
|
serializer = SiteSerializer(data=request.data["site"])
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
site = serializer.save()
|
||||||
|
|
||||||
|
# save custom fields
|
||||||
|
if "custom_fields" in request.data.keys():
|
||||||
|
|
||||||
|
for field in request.data["custom_fields"]:
|
||||||
|
|
||||||
|
custom_field = field
|
||||||
|
custom_field["site"] = site.id
|
||||||
|
|
||||||
|
serializer = SiteCustomFieldSerializer(data=custom_field)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
return Response(f"Site {site.name} was added!")
|
||||||
|
|
||||||
|
|
||||||
|
class GetUpdateSite(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
site = get_object_or_404(Site, pk=pk)
|
||||||
|
return Response(SiteSerializer(site).data)
|
||||||
|
|
||||||
|
def put(self, request, pk):
|
||||||
|
site = get_object_or_404(Site, pk=pk)
|
||||||
|
|
||||||
|
if "client" in request.data["site"].keys() and (
|
||||||
|
site.client.id != request.data["site"]["client"]
|
||||||
|
and site.client.sites.count() == 1
|
||||||
|
):
|
||||||
|
return notify_error("A client must have at least one site")
|
||||||
|
|
||||||
serializer = SiteSerializer(
|
serializer = SiteSerializer(
|
||||||
data={"name": name, "client": request.data["client"]},
|
instance=site, data=request.data["site"], partial=True
|
||||||
context={"clientpk": request.data["client"]},
|
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response("ok")
|
# update custom field
|
||||||
|
if "custom_fields" in request.data.keys():
|
||||||
|
|
||||||
|
for field in request.data["custom_fields"]:
|
||||||
|
|
||||||
class GetUpdateDeleteSite(APIView):
|
custom_field = field
|
||||||
def put(self, request, pk):
|
custom_field["site"] = pk
|
||||||
|
|
||||||
site = get_object_or_404(Site, pk=pk)
|
if SiteCustomField.objects.filter(field=field["field"], site=pk):
|
||||||
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
|
value = SiteCustomField.objects.get(field=field["field"], site=pk)
|
||||||
|
serializer = SiteCustomFieldSerializer(
|
||||||
|
instance=value, data=custom_field, partial=True
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
else:
|
||||||
|
serializer = SiteCustomFieldSerializer(data=custom_field)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response("ok")
|
return Response("Site was edited!")
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteSite(APIView):
|
||||||
|
def delete(self, request, pk, sitepk):
|
||||||
|
from automation.tasks import generate_all_agent_checks_task
|
||||||
|
|
||||||
def delete(self, request, pk):
|
|
||||||
site = get_object_or_404(Site, pk=pk)
|
site = get_object_or_404(Site, pk=pk)
|
||||||
if site.client.sites.count() == 1:
|
if site.client.sites.count() == 1:
|
||||||
return notify_error(f"A client must have at least 1 site.")
|
return notify_error("A client must have at least 1 site.")
|
||||||
|
|
||||||
agent_count = Agent.objects.filter(site=site).count()
|
agents = Agent.objects.filter(site=site)
|
||||||
|
|
||||||
if agent_count > 0:
|
if not sitepk:
|
||||||
return notify_error(
|
return notify_error(
|
||||||
f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
|
"There needs to be a site specified to move the agents to"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
agent_site = get_object_or_404(Site, pk=sitepk)
|
||||||
|
|
||||||
|
agents.update(site=agent_site)
|
||||||
|
|
||||||
|
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
|
||||||
|
generate_all_agent_checks_task.delay("server", create_tasks=True)
|
||||||
|
|
||||||
site.delete()
|
site.delete()
|
||||||
return Response(f"{site.name} was deleted!")
|
return Response(f"{site.name} was deleted!")
|
||||||
|
|
||||||
@@ -176,6 +283,8 @@ class GenerateAgent(APIView):
|
|||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def get(self, request, uid):
|
def get(self, request, uid):
|
||||||
|
from tacticalrmm.utils import generate_winagent_exe
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_ = uuid.UUID(uid, version=4)
|
_ = uuid.UUID(uid, version=4)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -183,99 +292,22 @@ class GenerateAgent(APIView):
|
|||||||
|
|
||||||
d = get_object_or_404(Deployment, uid=uid)
|
d = get_object_or_404(Deployment, uid=uid)
|
||||||
|
|
||||||
go_bin = "/usr/local/rmmgo/go/bin/go"
|
|
||||||
|
|
||||||
if not os.path.exists(go_bin):
|
|
||||||
return notify_error("Missing golang")
|
|
||||||
|
|
||||||
api = f"https://{request.get_host()}"
|
|
||||||
inno = (
|
|
||||||
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
|
||||||
if d.arch == "64"
|
|
||||||
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
|
||||||
)
|
|
||||||
download_url = settings.DL_64 if d.arch == "64" else settings.DL_32
|
|
||||||
|
|
||||||
client = d.client.name.replace(" ", "").lower()
|
client = d.client.name.replace(" ", "").lower()
|
||||||
site = d.site.name.replace(" ", "").lower()
|
site = d.site.name.replace(" ", "").lower()
|
||||||
client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
|
client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
|
||||||
site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
|
site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
|
||||||
|
|
||||||
ext = ".exe" if d.arch == "64" else "-x86.exe"
|
ext = ".exe" if d.arch == "64" else "-x86.exe"
|
||||||
|
|
||||||
file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
|
file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
|
||||||
exe = os.path.join(settings.EXE_DIR, file_name)
|
|
||||||
|
|
||||||
if os.path.exists(exe):
|
return generate_winagent_exe(
|
||||||
try:
|
client=d.client.pk,
|
||||||
os.remove(exe)
|
site=d.site.pk,
|
||||||
except:
|
agent_type=d.mon_type,
|
||||||
pass
|
rdp=d.install_flags["rdp"],
|
||||||
|
ping=d.install_flags["ping"],
|
||||||
goarch = "amd64" if d.arch == "64" else "386"
|
power=d.install_flags["power"],
|
||||||
cmd = [
|
arch=d.arch,
|
||||||
"env",
|
token=d.token_key,
|
||||||
"GOOS=windows",
|
api=f"https://{request.get_host()}",
|
||||||
f"GOARCH={goarch}",
|
file_name=file_name,
|
||||||
go_bin,
|
|
||||||
"build",
|
|
||||||
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
|
|
||||||
f"-X 'main.Api={api}'",
|
|
||||||
f"-X 'main.Client={d.client.pk}'",
|
|
||||||
f"-X 'main.Site={d.site.pk}'",
|
|
||||||
f"-X 'main.Atype={d.mon_type}'",
|
|
||||||
f"-X 'main.Rdp={d.install_flags['rdp']}'",
|
|
||||||
f"-X 'main.Ping={d.install_flags['ping']}'",
|
|
||||||
f"-X 'main.Power={d.install_flags['power']}'",
|
|
||||||
f"-X 'main.DownloadUrl={download_url}'",
|
|
||||||
f"-X 'main.Token={d.token_key}'\"",
|
|
||||||
"-o",
|
|
||||||
exe,
|
|
||||||
]
|
|
||||||
|
|
||||||
gen = [
|
|
||||||
"env",
|
|
||||||
"GOOS=windows",
|
|
||||||
f"GOARCH={goarch}",
|
|
||||||
go_bin,
|
|
||||||
"generate",
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
r1 = subprocess.run(
|
|
||||||
" ".join(gen),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
)
|
||||||
except:
|
|
||||||
return notify_error("genfailed")
|
|
||||||
|
|
||||||
if r1.returncode != 0:
|
|
||||||
return notify_error("genfailed")
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
" ".join(cmd),
|
|
||||||
capture_output=True,
|
|
||||||
shell=True,
|
|
||||||
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
return notify_error("buildfailed")
|
|
||||||
|
|
||||||
if r.returncode != 0:
|
|
||||||
return notify_error("buildfailed")
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
with open(exe, "rb") as f:
|
|
||||||
response = HttpResponse(
|
|
||||||
f.read(),
|
|
||||||
content_type="application/vnd.microsoft.portable-executable",
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = f"inline; filename={file_name}"
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
response = HttpResponse()
|
|
||||||
response["Content-Disposition"] = f"attachment; filename={file_name}"
|
|
||||||
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import CoreSettings
|
from .models import CoreSettings, CustomField
|
||||||
|
|
||||||
admin.site.register(CoreSettings)
|
admin.site.register(CoreSettings)
|
||||||
|
admin.site.register(CustomField)
|
||||||
|
|||||||
79
api/tacticalrmm/core/consumers.py
Normal file
79
api/tacticalrmm/core/consumers.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
from agents.models import Agent
|
||||||
|
|
||||||
|
|
||||||
|
class DashInfo(AsyncJsonWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
|
||||||
|
self.user = self.scope["user"]
|
||||||
|
|
||||||
|
if isinstance(self.user, AnonymousUser):
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
await self.accept()
|
||||||
|
self.connected = True
|
||||||
|
self.dash_info = asyncio.create_task(self.send_dash_info())
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dash_info.cancel()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.connected = False
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def receive(self, json_data=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_dashboard_info(self):
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"total_server_offline_count": server_offline_count,
|
||||||
|
"total_workstation_offline_count": workstation_offline_count,
|
||||||
|
"total_server_count": Agent.objects.filter(
|
||||||
|
monitoring_type="server"
|
||||||
|
).count(),
|
||||||
|
"total_workstation_count": Agent.objects.filter(
|
||||||
|
monitoring_type="workstation"
|
||||||
|
).count(),
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def send_dash_info(self):
|
||||||
|
while self.connected:
|
||||||
|
c = await self.get_dashboard_info()
|
||||||
|
await self.send_json(c)
|
||||||
|
await asyncio.sleep(30)
|
||||||
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
module github.com/wh1te909/goinstaller
|
|
||||||
|
|
||||||
go 1.16
|
|
||||||
|
|
||||||
require github.com/josephspurrier/goversioninfo v1.2.0 // indirect
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
|
||||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
|
|
||||||
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
||||||
<assemblyIdentity
|
|
||||||
type="win32"
|
|
||||||
name="TacticalRMMInstaller"
|
|
||||||
version="1.0.0.0"
|
|
||||||
processorArchitecture="*"/>
|
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<security>
|
|
||||||
<requestedPrivileges>
|
|
||||||
<requestedExecutionLevel
|
|
||||||
level="requireAdministrator"
|
|
||||||
uiAccess="false"/>
|
|
||||||
</requestedPrivileges>
|
|
||||||
</security>
|
|
||||||
</trustInfo>
|
|
||||||
</assembly>
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
//go:generate goversioninfo -icon=onit.ico -manifest=goversioninfo.exe.manifest -gofile=versioninfo.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Inno string
|
|
||||||
Api string
|
|
||||||
Client string
|
|
||||||
Site string
|
|
||||||
Atype string
|
|
||||||
Power string
|
|
||||||
Rdp string
|
|
||||||
Ping string
|
|
||||||
Token string
|
|
||||||
DownloadUrl string
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadAgent(filepath string) (err error) {
|
|
||||||
|
|
||||||
out, err := os.Create(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
resp, err := http.Get(DownloadUrl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("Bad response: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(out, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
debugLog := flag.String("log", "", "Verbose output")
|
|
||||||
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
|
|
||||||
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
|
|
||||||
cert := flag.String("cert", "", "Path to ca.pem")
|
|
||||||
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
var debug bool = false
|
|
||||||
|
|
||||||
if strings.TrimSpace(strings.ToLower(*debugLog)) == "debug" {
|
|
||||||
debug = true
|
|
||||||
}
|
|
||||||
|
|
||||||
agentBinary := filepath.Join(os.Getenv("windir"), "Temp", Inno)
|
|
||||||
tacrmm := filepath.Join(os.Getenv("PROGRAMFILES"), "TacticalAgent", "tacticalrmm.exe")
|
|
||||||
|
|
||||||
cmdArgs := []string{
|
|
||||||
"-m", "install", "--api", Api, "--client-id",
|
|
||||||
Client, "--site-id", Site, "--agent-type", Atype,
|
|
||||||
"--auth", Token,
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
cmdArgs = append(cmdArgs, "-log", "debug")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *silent {
|
|
||||||
cmdArgs = append(cmdArgs, "-silent")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.TrimSpace(*localMesh)) != 0 {
|
|
||||||
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.TrimSpace(*cert)) != 0 {
|
|
||||||
cmdArgs = append(cmdArgs, "-cert", *cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.TrimSpace(*timeout)) != 0 {
|
|
||||||
cmdArgs = append(cmdArgs, "-timeout", *timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if Rdp == "1" {
|
|
||||||
cmdArgs = append(cmdArgs, "-rdp")
|
|
||||||
}
|
|
||||||
|
|
||||||
if Ping == "1" {
|
|
||||||
cmdArgs = append(cmdArgs, "-ping")
|
|
||||||
}
|
|
||||||
|
|
||||||
if Power == "1" {
|
|
||||||
cmdArgs = append(cmdArgs, "-power")
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
fmt.Println("Installer:", agentBinary)
|
|
||||||
fmt.Println("Tactical Agent:", tacrmm)
|
|
||||||
fmt.Println("Download URL:", DownloadUrl)
|
|
||||||
fmt.Println("Install command:", tacrmm, strings.Join(cmdArgs, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Downloading agent...")
|
|
||||||
dl := downloadAgent(agentBinary)
|
|
||||||
if dl != nil {
|
|
||||||
fmt.Println("ERROR: unable to download agent from", DownloadUrl)
|
|
||||||
fmt.Println(dl)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer os.Remove(agentBinary)
|
|
||||||
|
|
||||||
fmt.Println("Extracting files...")
|
|
||||||
winagentCmd := exec.Command(agentBinary, "/VERYSILENT", "/SUPPRESSMSGBOXES")
|
|
||||||
err := winagentCmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
|
||||||
fmt.Println("Installation starting.")
|
|
||||||
cmd := exec.Command(tacrmm, cmdArgs...)
|
|
||||||
|
|
||||||
cmdReader, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdErrReader, oerr := cmd.StderrPipe()
|
|
||||||
if oerr != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, oerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(cmdReader)
|
|
||||||
escanner := bufio.NewScanner(cmdErrReader)
|
|
||||||
go func() {
|
|
||||||
for scanner.Scan() {
|
|
||||||
fmt.Println(scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for escanner.Scan() {
|
|
||||||
fmt.Println(escanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"FixedFileInfo": {
|
|
||||||
"FileVersion": {
|
|
||||||
"Major": 1,
|
|
||||||
"Minor": 0,
|
|
||||||
"Patch": 0,
|
|
||||||
"Build": 0
|
|
||||||
},
|
|
||||||
"ProductVersion": {
|
|
||||||
"Major": 1,
|
|
||||||
"Minor": 0,
|
|
||||||
"Patch": 0,
|
|
||||||
"Build": 0
|
|
||||||
},
|
|
||||||
"FileFlagsMask": "3f",
|
|
||||||
"FileFlags ": "00",
|
|
||||||
"FileOS": "040004",
|
|
||||||
"FileType": "01",
|
|
||||||
"FileSubType": "00"
|
|
||||||
},
|
|
||||||
"StringFileInfo": {
|
|
||||||
"Comments": "",
|
|
||||||
"CompanyName": "Tactical Techs",
|
|
||||||
"FileDescription": "Tactical RMM Installer",
|
|
||||||
"FileVersion": "v1.0.0.0",
|
|
||||||
"InternalName": "rmm.exe",
|
|
||||||
"LegalCopyright": "Copyright (c) 2020 Tactical Techs",
|
|
||||||
"LegalTrademarks": "",
|
|
||||||
"OriginalFilename": "installer.go",
|
|
||||||
"PrivateBuild": "",
|
|
||||||
"ProductName": "Tactical RMM Installer",
|
|
||||||
"ProductVersion": "v1.0.0.0",
|
|
||||||
"SpecialBuild": ""
|
|
||||||
},
|
|
||||||
"VarFileInfo": {
|
|
||||||
"Translation": {
|
|
||||||
"LangID": "0409",
|
|
||||||
"CharsetID": "04B0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IconPath": "",
|
|
||||||
"ManifestPath": ""
|
|
||||||
}
|
|
||||||
@@ -33,11 +33,20 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
|
|||||||
|
|
||||||
Try
|
Try
|
||||||
{
|
{
|
||||||
|
$DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled
|
||||||
|
if ($DefenderStatus -match "True") {
|
||||||
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
|
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
|
||||||
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
|
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
|
||||||
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
|
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
|
||||||
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
|
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
# pass
|
||||||
|
}
|
||||||
|
|
||||||
|
Try
|
||||||
|
{
|
||||||
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
||||||
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
|
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
|
||||||
write-host ('Extracting...')
|
write-host ('Extracting...')
|
||||||
|
|||||||
27
api/tacticalrmm/core/migrations/0014_customfield.py
Normal file
27
api/tacticalrmm/core/migrations/0014_customfield.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0013_coresettings_alert_template'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('model', models.CharField(choices=[('client', 'Client'), ('site', 'Site'), ('agent', 'Agent')], max_length=25)),
|
||||||
|
('type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('single', 'Single'), ('multiple', 'Multiple'), ('checkbox', 'Checkbox'), ('datetime', 'DateTime')], default='text', max_length=25)),
|
||||||
|
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||||
|
('name', models.TextField(blank=True, null=True)),
|
||||||
|
('default_value', models.TextField(blank=True, null=True)),
|
||||||
|
('required', models.BooleanField(blank=True, default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
Normal file
18
api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-18 20:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_customfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='order',
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
Normal file
17
api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-19 15:36
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0015_auto_20210318_2034'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='customfield',
|
||||||
|
unique_together={('model', 'name')},
|
||||||
|
),
|
||||||
|
]
|
||||||
24
api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
Normal file
24
api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 10:50
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0016_auto_20210319_1536'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='checkbox_value',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='default_values_multiple',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/tacticalrmm/core/migrations/0018_auto_20210329_1709.py
Normal file
23
api/tacticalrmm/core/migrations/0018_auto_20210329_1709.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0017_auto_20210329_1050'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customfield',
|
||||||
|
old_name='checkbox_value',
|
||||||
|
new_name='default_value_bool',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customfield',
|
||||||
|
old_name='default_value',
|
||||||
|
new_name='default_value_string',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -78,6 +78,7 @@ class CoreSettings(BaseAuditModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
from alerts.tasks import cache_agents_alert_template
|
||||||
from automation.tasks import generate_all_agent_checks_task
|
from automation.tasks import generate_all_agent_checks_task
|
||||||
|
|
||||||
if not self.pk and CoreSettings.objects.exists():
|
if not self.pk and CoreSettings.objects.exists():
|
||||||
@@ -105,6 +106,9 @@ class CoreSettings(BaseAuditModel):
|
|||||||
mon_type="workstation", create_tasks=True
|
mon_type="workstation", create_tasks=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if old_settings and old_settings.alert_template != self.alert_template:
|
||||||
|
cache_agents_alert_template.delay()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Global Site Settings"
|
return "Global Site Settings"
|
||||||
|
|
||||||
@@ -212,3 +216,53 @@ class CoreSettings(BaseAuditModel):
|
|||||||
from .serializers import CoreSerializer
|
from .serializers import CoreSerializer
|
||||||
|
|
||||||
return CoreSerializer(core).data
|
return CoreSerializer(core).data
|
||||||
|
|
||||||
|
|
||||||
|
FIELD_TYPE_CHOICES = (
|
||||||
|
("text", "Text"),
|
||||||
|
("number", "Number"),
|
||||||
|
("single", "Single"),
|
||||||
|
("multiple", "Multiple"),
|
||||||
|
("checkbox", "Checkbox"),
|
||||||
|
("datetime", "DateTime"),
|
||||||
|
)
|
||||||
|
|
||||||
|
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
|
||||||
|
|
||||||
|
|
||||||
|
class CustomField(models.Model):
|
||||||
|
|
||||||
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
model = models.CharField(max_length=25, choices=MODEL_CHOICES)
|
||||||
|
type = models.CharField(max_length=25, choices=FIELD_TYPE_CHOICES, default="text")
|
||||||
|
options = ArrayField(
|
||||||
|
models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
name = models.TextField(null=True, blank=True)
|
||||||
|
required = models.BooleanField(blank=True, default=False)
|
||||||
|
default_value_string = models.TextField(null=True, blank=True)
|
||||||
|
default_value_bool = models.BooleanField(default=False)
|
||||||
|
default_values_multiple = ArrayField(
|
||||||
|
models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (("model", "name"),)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_value(self):
|
||||||
|
if self.type == "multiple":
|
||||||
|
return self.default_values_multiple
|
||||||
|
elif self.type == "checkbox":
|
||||||
|
return self.default_value_bool
|
||||||
|
else:
|
||||||
|
return self.default_value_string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytz
|
import pytz
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import CoreSettings
|
from .models import CoreSettings, CustomField
|
||||||
|
|
||||||
|
|
||||||
class CoreSettingsSerializer(serializers.ModelSerializer):
|
class CoreSettingsSerializer(serializers.ModelSerializer):
|
||||||
@@ -21,3 +21,9 @@ class CoreSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CoreSettings
|
model = CoreSettings
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CustomField
|
||||||
|
fields = "__all__"
|
||||||
|
|||||||
@@ -1,11 +1,39 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from model_bakery import baker, seq
|
from channels.db import database_sync_to_async
|
||||||
|
from channels.testing import WebsocketCommunicator
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
from core.models import CoreSettings
|
|
||||||
from core.tasks import core_maintenance_tasks
|
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
|
from .consumers import DashInfo
|
||||||
|
from .models import CoreSettings, CustomField
|
||||||
|
from .serializers import CustomFieldSerializer
|
||||||
|
from .tasks import core_maintenance_tasks
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsumers(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_coresettings()
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_token(self):
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
token = Token.objects.create(user=self.john)
|
||||||
|
return token.key
|
||||||
|
|
||||||
|
async def test_dash_info(self):
|
||||||
|
key = self.get_token()
|
||||||
|
communicator = WebsocketCommunicator(
|
||||||
|
DashInfo.as_asgi(), f"/ws/dashinfo/?access_token={key}"
|
||||||
|
)
|
||||||
|
communicator.scope["user"] = self.john
|
||||||
|
connected, _ = await communicator.connect()
|
||||||
|
assert connected
|
||||||
|
await communicator.disconnect()
|
||||||
|
|
||||||
|
|
||||||
class TestCoreTasks(TacticalTestCase):
|
class TestCoreTasks(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -42,7 +70,7 @@ class TestCoreTasks(TacticalTestCase):
|
|||||||
url = "/core/editsettings/"
|
url = "/core/editsettings/"
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
policies = baker.make("Policy", _quantity=2)
|
policies = baker.make("automation.Policy", _quantity=2)
|
||||||
# test normal request
|
# test normal request
|
||||||
data = {
|
data = {
|
||||||
"smtp_from_email": "newexample@example.com",
|
"smtp_from_email": "newexample@example.com",
|
||||||
@@ -59,14 +87,14 @@ class TestCoreTasks(TacticalTestCase):
|
|||||||
|
|
||||||
# test adding policy
|
# test adding policy
|
||||||
data = {
|
data = {
|
||||||
"workstation_policy": policies[0].id,
|
"workstation_policy": policies[0].id, # type: ignore
|
||||||
"server_policy": policies[1].id,
|
"server_policy": policies[1].id, # type: ignore
|
||||||
}
|
}
|
||||||
r = self.client.patch(url, data)
|
r = self.client.patch(url, data)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id)
|
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
CoreSettings.objects.first().workstation_policy.id, policies[0].id
|
CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
|
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
|
||||||
@@ -128,3 +156,97 @@ class TestCoreTasks(TacticalTestCase):
|
|||||||
remove_orphaned_win_tasks.assert_called()
|
remove_orphaned_win_tasks.assert_called()
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
def test_get_custom_fields(self):
|
||||||
|
url = "/core/customfields/"
|
||||||
|
|
||||||
|
# setup
|
||||||
|
custom_fields = baker.make("core.CustomField", _quantity=2)
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
serializer = CustomFieldSerializer(custom_fields, many=True)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data), 2) # type: ignore
|
||||||
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
def test_get_custom_fields_by_model(self):
|
||||||
|
url = "/core/customfields/"
|
||||||
|
|
||||||
|
# setup
|
||||||
|
custom_fields = baker.make("core.CustomField", model="agent", _quantity=5)
|
||||||
|
baker.make("core.CustomField", model="client", _quantity=5)
|
||||||
|
|
||||||
|
# will error if request invalid
|
||||||
|
r = self.client.patch(url, {"invalid": ""})
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
|
data = {"model": "agent"}
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
serializer = CustomFieldSerializer(custom_fields, many=True)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data), 5) # type: ignore
|
||||||
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("patch", url)
|
||||||
|
|
||||||
|
def test_add_custom_field(self):
|
||||||
|
url = "/core/customfields/"
|
||||||
|
|
||||||
|
data = {"model": "client", "type": "text", "name": "Field"}
|
||||||
|
r = self.client.patch(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
def test_get_custom_field(self):
|
||||||
|
# setup
|
||||||
|
custom_field = baker.make("core.CustomField")
|
||||||
|
|
||||||
|
# test not found
|
||||||
|
r = self.client.get("/core/customfields/500/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
url = f"/core/customfields/{custom_field.id}/" # type: ignore
|
||||||
|
r = self.client.get(url)
|
||||||
|
serializer = CustomFieldSerializer(custom_field)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("get", url)
|
||||||
|
|
||||||
|
def test_update_custom_field(self):
|
||||||
|
# setup
|
||||||
|
custom_field = baker.make("core.CustomField")
|
||||||
|
|
||||||
|
# test not found
|
||||||
|
r = self.client.put("/core/customfields/500/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
url = f"/core/customfields/{custom_field.id}/" # type: ignore
|
||||||
|
data = {"type": "single", "options": ["ione", "two", "three"]}
|
||||||
|
r = self.client.put(url, data)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
new_field = CustomField.objects.get(pk=custom_field.id) # type: ignore
|
||||||
|
self.assertEqual(new_field.type, data["type"])
|
||||||
|
self.assertEqual(new_field.options, data["options"])
|
||||||
|
|
||||||
|
self.check_not_authenticated("put", url)
|
||||||
|
|
||||||
|
def test_delete_custom_field(self):
|
||||||
|
# setup
|
||||||
|
custom_field = baker.make("core.CustomField")
|
||||||
|
|
||||||
|
# test not found
|
||||||
|
r = self.client.delete("/core/customfields/500/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
url = f"/core/customfields/{custom_field.id}/" # type: ignore
|
||||||
|
r = self.client.delete(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore
|
||||||
|
|
||||||
|
self.check_not_authenticated("delete", url)
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ urlpatterns = [
|
|||||||
path("emailtest/", views.email_test),
|
path("emailtest/", views.email_test),
|
||||||
path("dashinfo/", views.dashboard_info),
|
path("dashinfo/", views.dashboard_info),
|
||||||
path("servermaintenance/", views.server_maintenance),
|
path("servermaintenance/", views.server_maintenance),
|
||||||
|
path("customfields/", views.GetAddCustomFields.as_view()),
|
||||||
|
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
@@ -10,8 +11,8 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
from tacticalrmm.utils import notify_error
|
from tacticalrmm.utils import notify_error
|
||||||
|
|
||||||
from .models import CoreSettings
|
from .models import CoreSettings, CustomField
|
||||||
from .serializers import CoreSettingsSerializer
|
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
|
||||||
|
|
||||||
|
|
||||||
class UploadMeshAgent(APIView):
|
class UploadMeshAgent(APIView):
|
||||||
@@ -63,6 +64,7 @@ def dashboard_info(request):
|
|||||||
"show_community_scripts": request.user.show_community_scripts,
|
"show_community_scripts": request.user.show_community_scripts,
|
||||||
"dbl_click_action": request.user.agent_dblclick_action,
|
"dbl_click_action": request.user.agent_dblclick_action,
|
||||||
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
|
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
|
||||||
|
"client_tree_sort": request.user.client_tree_sort,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,3 +134,46 @@ def server_maintenance(request):
|
|||||||
return Response(f"{records_count} records were pruned from the database")
|
return Response(f"{records_count} records were pruned from the database")
|
||||||
|
|
||||||
return notify_error("The data is incorrect")
|
return notify_error("The data is incorrect")
|
||||||
|
|
||||||
|
|
||||||
|
class GetAddCustomFields(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
fields = CustomField.objects.all()
|
||||||
|
return Response(CustomFieldSerializer(fields, many=True).data)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
if "model" in request.data.keys():
|
||||||
|
fields = CustomField.objects.filter(model=request.data["model"])
|
||||||
|
return Response(CustomFieldSerializer(fields, many=True).data)
|
||||||
|
else:
|
||||||
|
return notify_error("The request was invalid")
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = CustomFieldSerializer(data=request.data, partial=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
return Response("ok")
|
||||||
|
|
||||||
|
|
||||||
|
class GetUpdateDeleteCustomFields(APIView):
|
||||||
|
def get(self, request, pk):
|
||||||
|
custom_field = get_object_or_404(CustomField, pk=pk)
|
||||||
|
|
||||||
|
return Response(CustomFieldSerializer(custom_field).data)
|
||||||
|
|
||||||
|
def put(self, request, pk):
|
||||||
|
custom_field = get_object_or_404(CustomField, pk=pk)
|
||||||
|
|
||||||
|
serializer = CustomFieldSerializer(
|
||||||
|
instance=custom_field, 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(CustomField, pk=pk).delete()
|
||||||
|
|
||||||
|
return Response("ok")
|
||||||
|
|||||||
18
api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
Normal file
18
api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-02-28 09:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('logs', '0011_auto_20201119_0854'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pendingaction',
|
||||||
|
name='action_type',
|
||||||
|
field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install')], max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -9,6 +9,7 @@ ACTION_TYPE_CHOICES = [
|
|||||||
("schedreboot", "Scheduled Reboot"),
|
("schedreboot", "Scheduled Reboot"),
|
||||||
("taskaction", "Scheduled Task Action"),
|
("taskaction", "Scheduled Task Action"),
|
||||||
("agentupdate", "Agent Update"),
|
("agentupdate", "Agent Update"),
|
||||||
|
("chocoinstall", "Chocolatey Software Install"),
|
||||||
]
|
]
|
||||||
|
|
||||||
AUDIT_ACTION_TYPE_CHOICES = [
|
AUDIT_ACTION_TYPE_CHOICES = [
|
||||||
@@ -249,9 +250,12 @@ class PendingAction(models.Model):
|
|||||||
if self.action_type == "schedreboot":
|
if self.action_type == "schedreboot":
|
||||||
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
|
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
|
||||||
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
||||||
|
elif self.action_type == "taskaction":
|
||||||
elif self.action_type == "taskaction" or self.action_type == "agentupdate":
|
|
||||||
return "Next agent check-in"
|
return "Next agent check-in"
|
||||||
|
elif self.action_type == "agentupdate":
|
||||||
|
return "Next update cycle"
|
||||||
|
elif self.action_type == "chocoinstall":
|
||||||
|
return "ASAP"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
@@ -261,6 +265,9 @@ class PendingAction(models.Model):
|
|||||||
elif self.action_type == "agentupdate":
|
elif self.action_type == "agentupdate":
|
||||||
return f"Agent update to {self.details['version']}"
|
return f"Agent update to {self.details['version']}"
|
||||||
|
|
||||||
|
elif self.action_type == "chocoinstall":
|
||||||
|
return f"{self.details['name']} software install"
|
||||||
|
|
||||||
elif self.action_type == "taskaction":
|
elif self.action_type == "taskaction":
|
||||||
if self.details["action"] == "taskdelete":
|
if self.details["action"] == "taskdelete":
|
||||||
return "Device pending task deletion"
|
return "Device pending task deletion"
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from model_bakery import baker, seq
|
from model_bakery import baker, seq
|
||||||
|
|
||||||
|
from logs.models import PendingAction
|
||||||
from tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
from .serializers import PendingActionSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuditViews(TacticalTestCase):
|
class TestAuditViews(TacticalTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -177,63 +176,97 @@ class TestAuditViews(TacticalTestCase):
|
|||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
def test_agent_pending_actions(self):
|
def test_get_pending_actions(self):
|
||||||
agent = baker.make_recipe("agents.agent")
|
url = "/logs/pendingactions/"
|
||||||
pending_actions = baker.make(
|
agent1 = baker.make_recipe("agents.online_agent")
|
||||||
|
agent2 = baker.make_recipe("agents.online_agent")
|
||||||
|
|
||||||
|
baker.make(
|
||||||
"logs.PendingAction",
|
"logs.PendingAction",
|
||||||
agent=agent,
|
agent=agent1,
|
||||||
_quantity=6,
|
action_type="chocoinstall",
|
||||||
|
details={"name": "googlechrome", "output": None, "installed": False},
|
||||||
|
_quantity=12,
|
||||||
|
)
|
||||||
|
baker.make(
|
||||||
|
"logs.PendingAction",
|
||||||
|
agent=agent2,
|
||||||
|
action_type="chocoinstall",
|
||||||
|
status="completed",
|
||||||
|
details={"name": "adobereader", "output": None, "installed": False},
|
||||||
|
_quantity=14,
|
||||||
)
|
)
|
||||||
url = f"/logs/{agent.pk}/pendingactions/"
|
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
data = {"showCompleted": False}
|
||||||
serializer = PendingActionSerializer(pending_actions, many=True)
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
|
||||||
|
self.assertEqual(r.data["completed_count"], 14) # type: ignore
|
||||||
|
self.assertEqual(r.data["total"], 26) # type: ignore
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
PendingAction.objects.filter(action_type="chocoinstall").update(
|
||||||
self.assertEqual(len(resp.data), 6)
|
status="completed"
|
||||||
self.assertEqual(resp.data, serializer.data)
|
)
|
||||||
|
data = {"showCompleted": True}
|
||||||
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data["actions"]), 26) # type: ignore
|
||||||
|
self.assertEqual(r.data["completed_count"], 26) # type: ignore
|
||||||
|
self.assertEqual(r.data["total"], 26) # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
data = {"showCompleted": True, "agentPK": agent1.pk}
|
||||||
|
r = self.client.patch(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
|
||||||
|
self.assertEqual(r.data["completed_count"], 12) # type: ignore
|
||||||
|
self.assertEqual(r.data["total"], 12) # type: ignore
|
||||||
|
|
||||||
def test_all_pending_actions(self):
|
self.check_not_authenticated("patch", url)
|
||||||
url = "/logs/allpendingactions/"
|
|
||||||
agent = baker.make_recipe("agents.agent")
|
|
||||||
pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6)
|
|
||||||
|
|
||||||
resp = self.client.get(url, format="json")
|
|
||||||
serializer = PendingActionSerializer(pending_actions, many=True)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertEqual(len(resp.data), 6)
|
|
||||||
self.assertEqual(resp.data, serializer.data)
|
|
||||||
|
|
||||||
self.check_not_authenticated("get", url)
|
|
||||||
|
|
||||||
@patch("agents.models.Agent.nats_cmd")
|
@patch("agents.models.Agent.nats_cmd")
|
||||||
def test_cancel_pending_action(self, nats_cmd):
|
def test_cancel_pending_action(self, nats_cmd):
|
||||||
url = "/logs/cancelpendingaction/"
|
nats_cmd.return_value = "ok"
|
||||||
# TODO fix this TypeError: Object of type coroutine is not JSON serializable
|
url = "/logs/pendingactions/"
|
||||||
""" agent = baker.make("agents.Agent", version="1.1.1")
|
agent = baker.make_recipe("agents.online_agent")
|
||||||
pending_action = baker.make(
|
action = baker.make(
|
||||||
"logs.PendingAction",
|
"logs.PendingAction",
|
||||||
agent=agent,
|
agent=agent,
|
||||||
|
action_type="schedreboot",
|
||||||
details={
|
details={
|
||||||
"time": "2021-01-13 18:20:00",
|
"time": "2021-01-13 18:20:00",
|
||||||
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
|
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {"pk": pending_action.id}
|
data = {"pk": action.pk} # type: ignore
|
||||||
resp = self.client.delete(url, data, format="json")
|
r = self.client.delete(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
nats_data = {
|
nats_data = {
|
||||||
"func": "delschedtask",
|
"func": "delschedtask",
|
||||||
"schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
|
"schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
|
||||||
}
|
}
|
||||||
nats_cmd.assert_called_with(nats_data, timeout=10)
|
nats_cmd.assert_called_with(nats_data, timeout=10)
|
||||||
|
|
||||||
# try request again and it should fail since pending action doesn't exist
|
# try request again and it should 404 since pending action doesn't exist
|
||||||
resp = self.client.delete(url, data, format="json")
|
r = self.client.delete(url, data, format="json")
|
||||||
self.assertEqual(resp.status_code, 404) """
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
|
nats_cmd.reset_mock()
|
||||||
|
|
||||||
|
action2 = baker.make(
|
||||||
|
"logs.PendingAction",
|
||||||
|
agent=agent,
|
||||||
|
action_type="schedreboot",
|
||||||
|
details={
|
||||||
|
"time": "2021-01-13 18:20:00",
|
||||||
|
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {"pk": action2.pk} # type: ignore
|
||||||
|
nats_cmd.return_value = "error deleting sched task"
|
||||||
|
r = self.client.delete(url, data, format="json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
self.assertEqual(r.data, "error deleting sched task") # type: ignore
|
||||||
|
|
||||||
self.check_not_authenticated("delete", url)
|
self.check_not_authenticated("delete", url)
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("pendingactions/", views.PendingActions.as_view()),
|
||||||
path("auditlogs/", views.GetAuditLogs.as_view()),
|
path("auditlogs/", views.GetAuditLogs.as_view()),
|
||||||
path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()),
|
path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()),
|
||||||
path("<int:pk>/pendingactions/", views.agent_pending_actions),
|
|
||||||
path("allpendingactions/", views.all_pending_actions),
|
|
||||||
path("cancelpendingaction/", views.cancel_pending_action),
|
|
||||||
path("debuglog/<mode>/<hostname>/<order>/", views.debug_log),
|
path("debuglog/<mode>/<hostname>/<order>/", views.debug_log),
|
||||||
path("downloadlog/", views.download_log),
|
path("downloadlog/", views.download_log),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -106,24 +106,36 @@ class FilterOptionsAuditLog(APIView):
|
|||||||
return Response("error", status=status.HTTP_400_BAD_REQUEST)
|
return Response("error", status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
@api_view()
|
class PendingActions(APIView):
|
||||||
def agent_pending_actions(request, pk):
|
def patch(self, request):
|
||||||
action = PendingAction.objects.filter(agent__pk=pk)
|
status_filter = "completed" if request.data["showCompleted"] else "pending"
|
||||||
return Response(PendingActionSerializer(action, many=True).data)
|
if "agentPK" in request.data.keys():
|
||||||
|
actions = PendingAction.objects.filter(
|
||||||
|
agent__pk=request.data["agentPK"], status=status_filter
|
||||||
|
)
|
||||||
|
total = PendingAction.objects.filter(
|
||||||
|
agent__pk=request.data["agentPK"]
|
||||||
|
).count()
|
||||||
|
completed = PendingAction.objects.filter(
|
||||||
|
agent__pk=request.data["agentPK"], status="completed"
|
||||||
|
).count()
|
||||||
|
|
||||||
|
else:
|
||||||
|
actions = PendingAction.objects.filter(status=status_filter).select_related(
|
||||||
|
"agent"
|
||||||
|
)
|
||||||
|
total = PendingAction.objects.count()
|
||||||
|
completed = PendingAction.objects.filter(status="completed").count()
|
||||||
|
|
||||||
@api_view()
|
ret = {
|
||||||
def all_pending_actions(request):
|
"actions": PendingActionSerializer(actions, many=True).data,
|
||||||
actions = PendingAction.objects.all().select_related("agent")
|
"completed_count": completed,
|
||||||
return Response(PendingActionSerializer(actions, many=True).data)
|
"total": total,
|
||||||
|
}
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
@api_view(["DELETE"])
|
|
||||||
def cancel_pending_action(request):
|
|
||||||
action = get_object_or_404(PendingAction, pk=request.data["pk"])
|
action = get_object_or_404(PendingAction, pk=request.data["pk"])
|
||||||
if not action.agent.has_gotasks:
|
|
||||||
return notify_error("Requires agent version 1.1.1 or greater")
|
|
||||||
|
|
||||||
nats_data = {
|
nats_data = {
|
||||||
"func": "delschedtask",
|
"func": "delschedtask",
|
||||||
"schedtaskpayload": {"name": action.details["taskname"]},
|
"schedtaskpayload": {"name": action.details["taskname"]},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user