Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e48015a54 | ||
|
|
9ed3abf932 | ||
|
|
61762828a3 | ||
|
|
59beabe5ac | ||
|
|
0b30faa28c | ||
|
|
d12d49b93f | ||
|
|
f1d64d275a | ||
|
|
d094eeeb03 | ||
|
|
be25af658e | ||
|
|
794f52c229 | ||
|
|
5d4dc4ed4c | ||
|
|
e49d97b898 | ||
|
|
b6b4f1ba62 | ||
|
|
653d476716 | ||
|
|
48b855258c | ||
|
|
c7efdaf5f9 | ||
|
|
22523ed3d3 | ||
|
|
33c602dd61 | ||
|
|
e2a5509b76 | ||
|
|
61a0fa1a89 | ||
|
|
a35bd8292b | ||
|
|
06c8ae60e3 | ||
|
|
deeab1f845 | ||
|
|
da81c4c987 | ||
|
|
d180f1b2d5 | ||
|
|
526135629c | ||
|
|
6b9493e057 | ||
|
|
9bb33d2afc | ||
|
|
7421138533 | ||
|
|
d0800c52bb | ||
|
|
913fcd4df2 | ||
|
|
83322cc725 | ||
|
|
5944501feb | ||
|
|
17e3603d3d | ||
|
|
95be43ae47 | ||
|
|
feb91cbbaa | ||
|
|
79409af168 | ||
|
|
5dbfb64822 | ||
|
|
5e7ebf5e69 | ||
|
|
e73215ca74 | ||
|
|
a5f123b9ce | ||
|
|
ac058e9675 | ||
|
|
371b764d1d | ||
|
|
66d7172e09 | ||
|
|
99d3a8a749 | ||
|
|
db5ff372a4 | ||
|
|
3fe83f81be | ||
|
|
669e638fd6 | ||
|
|
f1f999f3b6 | ||
|
|
6f3b6fa9ce | ||
|
|
938f945301 | ||
|
|
e3efb2aad6 | ||
|
|
1e678c0d78 | ||
|
|
a59c111140 | ||
|
|
a8b2a31bed | ||
|
|
37402f9ee8 | ||
|
|
e7b5ecb40f | ||
|
|
c817ef04b9 | ||
|
|
f52b18439c | ||
|
|
1e03c628d5 | ||
|
|
71fb39db1f | ||
|
|
bcfb3726b0 | ||
|
|
c6e9e29671 | ||
|
|
1bfefcce39 | ||
|
|
22488e93e1 | ||
|
|
244b89f035 | ||
|
|
1f9a241b94 | ||
|
|
03641aae42 | ||
|
|
a2bdd113cc | ||
|
|
a92e2f3c7b | ||
|
|
97766b3a57 | ||
|
|
9ef4c3bb06 | ||
|
|
d82f0cd757 | ||
|
|
5f529e2af4 | ||
|
|
beadd9e02b | ||
|
|
72543789cb | ||
|
|
5789439fa9 | ||
|
|
f549126bcf | ||
|
|
7197548bad | ||
|
|
241fde783c | ||
|
|
2b872cd1f4 | ||
|
|
a606fb4d1d | ||
|
|
9f9c6be38e | ||
|
|
01ee524049 | ||
|
|
af9cb65338 | ||
|
|
8aa11c580b | ||
|
|
ada627f444 | ||
|
|
a7b6d338c3 | ||
|
|
9f00538b97 | ||
|
|
a085015282 | ||
|
|
0b9c220fbb | ||
|
|
0e3d04873d | ||
|
|
b7578d939f | ||
|
|
b5c28de03f | ||
|
|
e17d25c156 | ||
|
|
c25dc1b99c | ||
|
|
a493a574bd | ||
|
|
4284493dce | ||
|
|
25059de8e1 | ||
|
|
1731b05ad0 | ||
|
|
e80dc663ac | ||
|
|
39988a4c2f | ||
|
|
415bff303a | ||
|
|
a65eb62a54 | ||
|
|
03b2982128 |
@@ -23,6 +23,6 @@ POSTGRES_USER=postgres
|
||||
POSTGRES_PASS=postgrespass
|
||||
|
||||
# DEV SETTINGS
|
||||
APP_PORT=8000
|
||||
API_PORT=8080
|
||||
APP_PORT=80
|
||||
API_PORT=80
|
||||
HTTP_PROTOCOL=https
|
||||
|
||||
@@ -3,6 +3,7 @@ version: '3.4'
|
||||
services:
|
||||
api-dev:
|
||||
image: api-dev
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./api.dockerfile
|
||||
@@ -21,6 +22,7 @@ services:
|
||||
|
||||
app-dev:
|
||||
image: node:12-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
|
||||
@@ -45,7 +45,7 @@ function django_setup {
|
||||
echo "setting up django environment"
|
||||
|
||||
# configure django settings
|
||||
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
|
||||
MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)"
|
||||
|
||||
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
|
||||
|
||||
@@ -106,29 +106,28 @@ EOF
|
||||
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
|
||||
|
||||
# run migrations and init scripts
|
||||
python manage.py migrate --no-input
|
||||
python manage.py collectstatic --no-input
|
||||
python manage.py initial_db_setup
|
||||
python manage.py initial_mesh_setup
|
||||
python manage.py load_chocos
|
||||
python manage.py load_community_scripts
|
||||
python manage.py reload_nats
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
|
||||
|
||||
# create super user
|
||||
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
|
||||
|
||||
}
|
||||
|
||||
if [ "$1" = 'tactical-init-dev' ]; then
|
||||
|
||||
# make directories if they don't exist
|
||||
mkdir -p ${TACTICAL_DIR}/tmp
|
||||
mkdir -p "${TACTICAL_DIR}/tmp"
|
||||
|
||||
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
|
||||
|
||||
# setup Python virtual env and install dependencies
|
||||
test -f ${VIRTUAL_ENV} && python -m venv --copies ${VIRTUAL_ENV}
|
||||
pip install --no-cache-dir -r /requirements.txt
|
||||
! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
|
||||
"${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
django_setup
|
||||
|
||||
@@ -150,20 +149,20 @@ EOF
|
||||
fi
|
||||
|
||||
if [ "$1" = 'tactical-api' ]; then
|
||||
cp ${WORKSPACE_DIR}/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
|
||||
cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
|
||||
chmod +x /usr/local/bin/goversioninfo
|
||||
|
||||
check_tactical_ready
|
||||
python manage.py runserver 0.0.0.0:${API_PORT}
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
|
||||
fi
|
||||
|
||||
if [ "$1" = 'tactical-celery-dev' ]; then
|
||||
check_tactical_ready
|
||||
env/bin/celery -A tacticalrmm worker -l debug
|
||||
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug
|
||||
fi
|
||||
|
||||
if [ "$1" = 'tactical-celerybeat-dev' ]; then
|
||||
check_tactical_ready
|
||||
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
|
||||
env/bin/celery -A tacticalrmm beat -l debug
|
||||
"${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
|
||||
fi
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
||||
amqp==2.6.1
|
||||
amqp==5.0.5
|
||||
asgiref==3.3.1
|
||||
asyncio-nats-client==0.11.4
|
||||
billiard==3.6.3.0
|
||||
celery==4.4.6
|
||||
celery==5.0.5
|
||||
certifi==2020.12.5
|
||||
cffi==1.14.3
|
||||
chardet==3.0.4
|
||||
cryptography==3.2.1
|
||||
cffi==1.14.5
|
||||
chardet==4.0.0
|
||||
cryptography==3.4.4
|
||||
decorator==4.4.2
|
||||
Django==3.1.4
|
||||
django-cors-headers==3.5.0
|
||||
Django==3.1.6
|
||||
django-cors-headers==3.7.0
|
||||
django-rest-knox==4.1.0
|
||||
djangorestframework==3.12.2
|
||||
future==0.18.2
|
||||
idna==2.10
|
||||
kombu==4.6.11
|
||||
kombu==5.0.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.0
|
||||
packaging==20.4
|
||||
msgpack==1.0.2
|
||||
packaging==20.8
|
||||
psycopg2-binary==2.8.6
|
||||
pycparser==2.20
|
||||
pycryptodome==3.9.9
|
||||
pyotp==2.4.1
|
||||
pycryptodome==3.10.1
|
||||
pyotp==2.6.0
|
||||
pyparsing==2.4.7
|
||||
pytz==2020.4
|
||||
pytz==2021.1
|
||||
qrcode==6.1
|
||||
redis==3.5.3
|
||||
requests==2.25.0
|
||||
requests==2.25.1
|
||||
six==1.15.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.49.0
|
||||
urllib3==1.26.2
|
||||
validators==0.18.1
|
||||
vine==1.3.0
|
||||
twilio==6.52.0
|
||||
urllib3==1.26.3
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
websockets==8.1
|
||||
zipp==3.4.0
|
||||
black
|
||||
@@ -42,3 +41,6 @@ django-extensions
|
||||
coverage
|
||||
coveralls
|
||||
model_bakery
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
pymdown-extensions
|
||||
|
||||
@@ -36,7 +36,7 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
- VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
|
||||
- VPS with 2GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
|
||||
- A domain you own with at least 3 subdomains
|
||||
- Google Authenticator app (2 factor is NOT optional)
|
||||
|
||||
|
||||
@@ -278,15 +278,11 @@ class TestUserAction(TacticalTestCase):
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data = {"agent_dblclick_action": "editagent"}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data = {"agent_dblclick_action": "remotebg"}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
data = {"agent_dblclick_action": "takecontrol"}
|
||||
data = {
|
||||
"userui": True,
|
||||
"agent_dblclick_action": "editagent",
|
||||
"default_agent_tbl_tab": "mixed",
|
||||
}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
||||
from .models import Agent, RecoveryAction, Note
|
||||
|
||||
admin.site.register(Agent)
|
||||
admin.site.register(AgentOutage)
|
||||
admin.site.register(RecoveryAction)
|
||||
admin.site.register(Note)
|
||||
|
||||
@@ -3,19 +3,20 @@ import string
|
||||
import os
|
||||
import json
|
||||
|
||||
from model_bakery.recipe import Recipe, seq
|
||||
from model_bakery.recipe import Recipe, foreign_key
|
||||
from itertools import cycle
|
||||
from django.utils import timezone as djangotime
|
||||
from django.conf import settings
|
||||
|
||||
from .models import Agent
|
||||
|
||||
|
||||
def generate_agent_id(hostname):
|
||||
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
|
||||
return f"{rand}-{hostname}"
|
||||
|
||||
|
||||
site = Recipe("clients.Site")
|
||||
|
||||
|
||||
def get_wmi_data():
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
|
||||
@@ -24,7 +25,8 @@ def get_wmi_data():
|
||||
|
||||
|
||||
agent = Recipe(
|
||||
Agent,
|
||||
"agents.Agent",
|
||||
site=foreign_key(site),
|
||||
hostname="DESKTOP-TEST123",
|
||||
version="1.3.0",
|
||||
monitoring_type=cycle(["workstation", "server"]),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-29 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0026_auto_20201125_2334'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='overdue_dashboard_alert',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
23
api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
Normal file
23
api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-06 15:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0027_agent_overdue_dashboard_alert'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agentoutage',
|
||||
name='outage_email_sent_time',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='agentoutage',
|
||||
name='outage_sms_sent_time',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
16
api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
Normal file
16
api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-10 21:56
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0028_auto_20210206_1534'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='AgentOutage',
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
Normal file
18
api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-16 08:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0029_delete_agentoutage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='offline_time',
|
||||
field=models.PositiveIntegerField(default=4),
|
||||
),
|
||||
]
|
||||
@@ -8,8 +8,10 @@ import validators
|
||||
import msgpack
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import List
|
||||
from typing import List, Union, Any
|
||||
from loguru import logger
|
||||
import asyncio
|
||||
|
||||
from packaging import version as pyver
|
||||
from distutils.version import LooseVersion
|
||||
from nats.aio.client import Client as NATS
|
||||
@@ -18,6 +20,7 @@ from nats.aio.errors import ErrTimeout
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from alerts.models import AlertTemplate
|
||||
|
||||
from core.models import CoreSettings, TZ_CHOICES
|
||||
from logs.models import BaseAuditModel
|
||||
@@ -50,6 +53,8 @@ class Agent(BaseAuditModel):
|
||||
mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
|
||||
overdue_email_alert = models.BooleanField(default=False)
|
||||
overdue_text_alert = models.BooleanField(default=False)
|
||||
overdue_dashboard_alert = models.BooleanField(default=False)
|
||||
offline_time = models.PositiveIntegerField(default=4)
|
||||
overdue_time = models.PositiveIntegerField(default=30)
|
||||
check_interval = models.PositiveIntegerField(default=120)
|
||||
needs_reboot = models.BooleanField(default=False)
|
||||
@@ -75,6 +80,24 @@ class Agent(BaseAuditModel):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# get old agent if exists
|
||||
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
# check if new agent has been create
|
||||
# or check if policy have changed on agent
|
||||
# or if site has changed on agent and if so generate-policies
|
||||
if (
|
||||
not old_agent
|
||||
or old_agent
|
||||
and old_agent.policy != self.policy
|
||||
or old_agent.site != self.site
|
||||
):
|
||||
self.generate_checks_from_policies()
|
||||
self.generate_tasks_from_policies()
|
||||
|
||||
def __str__(self):
|
||||
return self.hostname
|
||||
|
||||
@@ -127,7 +150,7 @@ class Agent(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
offline = djangotime.now() - djangotime.timedelta(minutes=6)
|
||||
offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
|
||||
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
|
||||
|
||||
if self.last_seen is not None:
|
||||
@@ -248,6 +271,63 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
return ["unknown disk"]
|
||||
|
||||
def run_script(
|
||||
self,
|
||||
scriptpk: int,
|
||||
args: List[str] = [],
|
||||
timeout: int = 120,
|
||||
full: bool = False,
|
||||
wait: bool = False,
|
||||
run_on_any: bool = False,
|
||||
) -> Any:
|
||||
|
||||
from scripts.models import Script
|
||||
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
data = {
|
||||
"func": "runscriptfull" if full else "runscript",
|
||||
"timeout": timeout,
|
||||
"script_args": args,
|
||||
"payload": {
|
||||
"code": script.code,
|
||||
"shell": script.shell,
|
||||
},
|
||||
}
|
||||
|
||||
running_agent = self
|
||||
if run_on_any:
|
||||
nats_ping = {"func": "ping", "timeout": 1}
|
||||
|
||||
# try on self first
|
||||
r = asyncio.run(self.nats_cmd(nats_ping))
|
||||
|
||||
if r == "pong":
|
||||
running_agent = self
|
||||
else:
|
||||
online = [
|
||||
agent
|
||||
for agent in Agent.objects.only(
|
||||
"pk", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
if agent.status == "online"
|
||||
]
|
||||
|
||||
for agent in online:
|
||||
r = asyncio.run(agent.nats_cmd(nats_ping))
|
||||
if r == "pong":
|
||||
running_agent = agent
|
||||
break
|
||||
|
||||
if running_agent.pk == self.pk:
|
||||
return "Unable to find an online agent"
|
||||
|
||||
if wait:
|
||||
return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
|
||||
else:
|
||||
asyncio.run(running_agent.nats_cmd(data, wait=False))
|
||||
|
||||
return "ok"
|
||||
|
||||
# auto approves updates
|
||||
def approve_updates(self):
|
||||
patch_policy = self.get_patch_policy()
|
||||
@@ -381,6 +461,105 @@ class Agent(BaseAuditModel):
|
||||
)
|
||||
)
|
||||
|
||||
# returns alert template assigned in the following order: policy, site, client, global
|
||||
# will return None if nothing is found
|
||||
def get_alert_template(self) -> Union[AlertTemplate, None]:
|
||||
|
||||
site = self.site
|
||||
client = self.client
|
||||
core = CoreSettings.objects.first()
|
||||
|
||||
templates = list()
|
||||
# check if alert template is on a policy assigned to agent
|
||||
if (
|
||||
self.policy
|
||||
and self.policy.alert_template
|
||||
and self.policy.alert_template.is_active
|
||||
):
|
||||
templates.append(self.policy.alert_template)
|
||||
|
||||
# check if policy with alert template is assigned to the site
|
||||
elif (
|
||||
self.monitoring_type == "server"
|
||||
and site.server_policy
|
||||
and site.server_policy.alert_template
|
||||
and site.server_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(site.server_policy.alert_template)
|
||||
elif (
|
||||
self.monitoring_type == "workstation"
|
||||
and site.workstation_policy
|
||||
and site.workstation_policy.alert_template
|
||||
and site.workstation_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(site.workstation_policy.alert_template)
|
||||
|
||||
# check if alert template is assigned to site
|
||||
elif site.alert_template and site.alert_template.is_active:
|
||||
templates.append(site.alert_template)
|
||||
|
||||
# check if policy with alert template is assigned to the client
|
||||
elif (
|
||||
self.monitoring_type == "server"
|
||||
and client.server_policy
|
||||
and client.server_policy.alert_template
|
||||
and client.server_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(client.server_policy.alert_template)
|
||||
elif (
|
||||
self.monitoring_type == "workstation"
|
||||
and client.workstation_policy
|
||||
and client.workstation_policy.alert_template
|
||||
and client.workstation_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(client.workstation_policy.alert_template)
|
||||
|
||||
# check if alert template is on client and return
|
||||
elif client.alert_template and client.alert_template.is_active:
|
||||
templates.append(client.alert_template)
|
||||
|
||||
# check if alert template is applied globally and return
|
||||
elif core.alert_template and core.alert_template.is_active:
|
||||
templates.append(core.alert_template)
|
||||
|
||||
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
|
||||
elif (
|
||||
self.monitoring_type == "server"
|
||||
and core.server_policy
|
||||
and core.server_policy.alert_template
|
||||
and core.server_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(core.server_policy.alert_template)
|
||||
elif (
|
||||
self.monitoring_type == "workstation"
|
||||
and core.workstation_policy
|
||||
and core.workstation_policy.alert_template
|
||||
and core.workstation_policy.alert_template.is_active
|
||||
):
|
||||
templates.append(core.workstation_policy.alert_template)
|
||||
|
||||
# go through the templates and return the first one that isn't excluded
|
||||
for template in templates:
|
||||
# check if client, site, or agent has been excluded from template
|
||||
if (
|
||||
client.pk in template.excluded_clients.all()
|
||||
or site.pk in template.excluded_sites.all()
|
||||
or self.pk in template.excluded_agents.all()
|
||||
):
|
||||
continue
|
||||
|
||||
# see if template is excluding desktops
|
||||
if (
|
||||
self.monitoring_type == "workstation"
|
||||
and not template.agent_include_desktops
|
||||
):
|
||||
continue
|
||||
else:
|
||||
return template
|
||||
|
||||
# no alert templates found or agent has been excluded
|
||||
return None
|
||||
|
||||
def generate_checks_from_policies(self):
|
||||
from automation.models import Policy
|
||||
|
||||
@@ -520,73 +699,203 @@ class Agent(BaseAuditModel):
|
||||
if action.details["task_id"] == task_id:
|
||||
action.delete()
|
||||
|
||||
def handle_alert(self, checkin: bool = False) -> None:
|
||||
from alerts.models import Alert
|
||||
from agents.tasks import (
|
||||
agent_recovery_email_task,
|
||||
agent_recovery_sms_task,
|
||||
agent_outage_email_task,
|
||||
agent_outage_sms_task,
|
||||
)
|
||||
|
||||
class AgentOutage(models.Model):
|
||||
agent = models.ForeignKey(
|
||||
Agent,
|
||||
related_name="agentoutages",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
outage_time = models.DateTimeField(auto_now_add=True)
|
||||
recovery_time = models.DateTimeField(null=True, blank=True)
|
||||
outage_email_sent = models.BooleanField(default=False)
|
||||
outage_sms_sent = models.BooleanField(default=False)
|
||||
recovery_email_sent = models.BooleanField(default=False)
|
||||
recovery_sms_sent = models.BooleanField(default=False)
|
||||
# return if agent is in maintenace mode
|
||||
if self.maintenance_mode:
|
||||
return
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return False if self.recovery_time else True
|
||||
alert_template = self.get_alert_template()
|
||||
|
||||
# called when agent is back online
|
||||
if checkin:
|
||||
if Alert.objects.filter(agent=self, resolved=False).exists():
|
||||
|
||||
# resolve alert if exists
|
||||
alert = Alert.objects.get(agent=self, resolved=False)
|
||||
alert.resolve()
|
||||
|
||||
# check if a resolved notification should be emailed
|
||||
if (
|
||||
not alert.resolved_email_sent
|
||||
and alert_template
|
||||
and alert_template.agent_email_on_resolved
|
||||
or self.overdue_email_alert
|
||||
):
|
||||
agent_recovery_email_task.delay(pk=alert.pk)
|
||||
|
||||
# check if a resolved notification should be texted
|
||||
if (
|
||||
not alert.resolved_sms_sent
|
||||
and alert_template
|
||||
and alert_template.agent_text_on_resolved
|
||||
or self.overdue_text_alert
|
||||
):
|
||||
agent_recovery_sms_task.delay(pk=alert.pk)
|
||||
|
||||
# check if any scripts should be run
|
||||
if (
|
||||
not alert.resolved_action_run
|
||||
and alert_template
|
||||
and alert_template.resolved_action
|
||||
):
|
||||
r = self.run_script(
|
||||
scriptpk=alert_template.resolved_action.pk,
|
||||
args=alert_template.resolved_action_args,
|
||||
timeout=alert_template.resolved_action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.resolved_action_retcode = r["retcode"]
|
||||
alert.resolved_action_stdout = r["stdout"]
|
||||
alert.resolved_action_stderr = r["stderr"]
|
||||
alert.resolved_action_execution_time = "{:.4f}".format(
|
||||
r["execution_time"]
|
||||
)
|
||||
alert.resolved_action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
|
||||
)
|
||||
|
||||
# called when agent is offline
|
||||
else:
|
||||
# check if alert hasn't been created yet so create it
|
||||
if not Alert.objects.filter(agent=self, resolved=False).exists():
|
||||
|
||||
alert = Alert.create_availability_alert(self)
|
||||
|
||||
# add a null check history to allow gaps in graph
|
||||
for check in self.agentchecks.all():
|
||||
check.add_check_history(None)
|
||||
else:
|
||||
alert = Alert.objects.get(agent=self, resolved=False)
|
||||
|
||||
# create dashboard alert if enabled
|
||||
if (
|
||||
alert_template
|
||||
and alert_template.agent_always_alert
|
||||
or self.overdue_dashboard_alert
|
||||
):
|
||||
alert.hidden = False
|
||||
alert.save()
|
||||
|
||||
# send email alert if enabled
|
||||
if (
|
||||
not alert.email_sent
|
||||
and alert_template
|
||||
and alert_template.agent_always_email
|
||||
or self.overdue_email_alert
|
||||
):
|
||||
agent_outage_email_task.delay(
|
||||
pk=alert.pk,
|
||||
alert_interval=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# send text message if enabled
|
||||
if (
|
||||
not alert.sms_sent
|
||||
and alert_template
|
||||
and alert_template.agent_always_text
|
||||
or self.overdue_text_alert
|
||||
):
|
||||
agent_outage_sms_task.delay(
|
||||
pk=alert.pk,
|
||||
alert_interval=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# check if any scripts should be run
|
||||
if not alert.action_run and alert_template and alert_template.action:
|
||||
r = self.run_script(
|
||||
scriptpk=alert_template.action.pk,
|
||||
args=alert_template.action_args,
|
||||
timeout=alert_template.action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.action_retcode = r["retcode"]
|
||||
alert.action_stdout = r["stdout"]
|
||||
alert.action_stderr = r["stderr"]
|
||||
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
|
||||
alert.action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
|
||||
)
|
||||
|
||||
def send_outage_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_mail(
|
||||
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue",
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||
(
|
||||
f"Data has not been received from client {self.agent.client.name}, "
|
||||
f"site {self.agent.site.name}, "
|
||||
f"agent {self.agent.hostname} "
|
||||
f"Data has not been received from client {self.client.name}, "
|
||||
f"site {self.site.name}, "
|
||||
f"agent {self.hostname} "
|
||||
"within the expected time."
|
||||
),
|
||||
alert_template=alert_template,
|
||||
)
|
||||
|
||||
def send_recovery_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_mail(
|
||||
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received",
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||
(
|
||||
f"Data has been received from client {self.agent.client.name}, "
|
||||
f"site {self.agent.site.name}, "
|
||||
f"agent {self.agent.hostname} "
|
||||
f"Data has been received from client {self.client.name}, "
|
||||
f"site {self.site.name}, "
|
||||
f"agent {self.hostname} "
|
||||
"after an interruption in data transmission."
|
||||
),
|
||||
alert_template=alert_template,
|
||||
)
|
||||
|
||||
def send_outage_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.get_alert_template()
|
||||
CORE = CoreSettings.objects.first()
|
||||
CORE.send_sms(
|
||||
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue"
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
|
||||
alert_template=alert_template,
|
||||
)
|
||||
|
||||
def send_recovery_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.get_alert_template()
|
||||
CORE.send_sms(
|
||||
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received"
|
||||
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
|
||||
alert_template=alert_template,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.agent.hostname
|
||||
|
||||
|
||||
RECOVERY_CHOICES = [
|
||||
("salt", "Salt"),
|
||||
|
||||
@@ -34,6 +34,17 @@ class AgentSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class AgentOverdueActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"pk",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
]
|
||||
|
||||
|
||||
class AgentTableSerializer(serializers.ModelSerializer):
|
||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
||||
pending_actions = serializers.SerializerMethodField()
|
||||
@@ -44,6 +55,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
site_name = serializers.ReadOnlyField(source="site.name")
|
||||
logged_username = serializers.SerializerMethodField()
|
||||
italic = serializers.SerializerMethodField()
|
||||
policy = serializers.ReadOnlyField(source="policy.id")
|
||||
|
||||
def get_pending_actions(self, obj):
|
||||
return obj.pendingactions.filter(status="pending").count()
|
||||
@@ -83,12 +95,14 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"status",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_dashboard_alert",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"checks",
|
||||
"maintenance_mode",
|
||||
"logged_username",
|
||||
"italic",
|
||||
"policy",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
@@ -114,10 +128,12 @@ class AgentEditSerializer(serializers.ModelSerializer):
|
||||
"timezone",
|
||||
"check_interval",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"all_timezones",
|
||||
"winupdatepolicy",
|
||||
"policy",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -3,63 +3,35 @@ from loguru import logger
|
||||
from time import sleep
|
||||
import random
|
||||
from packaging import version as pyver
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
import datetime as dt
|
||||
|
||||
from django.utils import timezone as djangotime
|
||||
from django.conf import settings
|
||||
from scripts.models import Script
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
from agents.models import Agent, AgentOutage
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from logs.models import PendingAction
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
def _check_agent_service(pk: int) -> None:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||
if r == "pong":
|
||||
logger.info(
|
||||
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery"
|
||||
)
|
||||
data = {"func": "recover", "payload": {"mode": "tacagent"}}
|
||||
asyncio.run(agent.nats_cmd(data, wait=False))
|
||||
|
||||
|
||||
def _check_in_full(pk: int) -> None:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False))
|
||||
|
||||
|
||||
@app.task
|
||||
def check_in_task() -> None:
|
||||
q = Agent.objects.only("pk", "version")
|
||||
agents: List[int] = [
|
||||
i.pk for i in q if pyver.parse(i.version) == pyver.parse("1.1.12")
|
||||
]
|
||||
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
_check_in_full(pk)
|
||||
sleep(0.1)
|
||||
rand = random.randint(3, 7)
|
||||
sleep(rand)
|
||||
|
||||
|
||||
@app.task
|
||||
def monitor_agents_task() -> None:
|
||||
q = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
|
||||
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
|
||||
for agent in agents:
|
||||
_check_agent_service(agent)
|
||||
|
||||
|
||||
def agent_update(pk: int) -> str:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
|
||||
logger.warning(
|
||||
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
|
||||
)
|
||||
return "not supported"
|
||||
|
||||
# skip if we can't determine the arch
|
||||
if agent.arch is None:
|
||||
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
|
||||
logger.warning(
|
||||
f"Unable to determine arch on {agent.hostname}. Skipping agent update."
|
||||
)
|
||||
return "noarch"
|
||||
|
||||
# removed sqlite in 1.4.0 to get rid of cgo dependency
|
||||
@@ -75,51 +47,38 @@ def agent_update(pk: int) -> str:
|
||||
)
|
||||
url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
|
||||
|
||||
if agent.has_nats:
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
action = agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).last()
|
||||
if pyver.parse(action.details["version"]) < pyver.parse(version):
|
||||
action.delete()
|
||||
else:
|
||||
return "pending"
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).delete()
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
else:
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
}
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
|
||||
return "created"
|
||||
|
||||
return "not supported"
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
}
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
return "created"
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks: List[int], version: str) -> None:
|
||||
q = Agent.objects.filter(pk__in=pks)
|
||||
agents: List[int] = [
|
||||
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
|
||||
]
|
||||
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
|
||||
def send_agent_update_task(pks: List[int]) -> None:
|
||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk)
|
||||
@@ -149,103 +108,94 @@ def auto_self_agent_update_task() -> None:
|
||||
|
||||
|
||||
@app.task
|
||||
def get_wmi_task():
|
||||
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
|
||||
online = [
|
||||
i
|
||||
for i in agents
|
||||
if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
|
||||
]
|
||||
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
|
||||
for chunk in chunks:
|
||||
for agent in chunk:
|
||||
asyncio.run(agent.nats_cmd({"func": "wmi"}, wait=False))
|
||||
sleep(0.1)
|
||||
rand = random.randint(3, 7)
|
||||
sleep(rand)
|
||||
def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
if not alert.email_sent:
|
||||
sleep(random.randint(1, 15))
|
||||
alert.agent.send_outage_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send an email only if the last email sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.email_sent < delta:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.agent.send_outage_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def sync_sysinfo_task():
|
||||
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
|
||||
online = [
|
||||
i
|
||||
for i in agents
|
||||
if pyver.parse(i.version) >= pyver.parse("1.1.3")
|
||||
and pyver.parse(i.version) <= pyver.parse("1.1.12")
|
||||
and i.status == "online"
|
||||
]
|
||||
def agent_recovery_email_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
|
||||
for chunk in chunks:
|
||||
for agent in chunk:
|
||||
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
|
||||
sleep(0.1)
|
||||
rand = random.randint(3, 7)
|
||||
sleep(rand)
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_outage_email_task(pk):
|
||||
sleep(random.randint(1, 15))
|
||||
outage = AgentOutage.objects.get(pk=pk)
|
||||
outage.send_outage_email()
|
||||
outage.outage_email_sent = True
|
||||
outage.save(update_fields=["outage_email_sent"])
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
alert.agent.send_recovery_email()
|
||||
alert.resolved_email_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_recovery_email_task(pk):
|
||||
sleep(random.randint(1, 15))
|
||||
outage = AgentOutage.objects.get(pk=pk)
|
||||
outage.send_recovery_email()
|
||||
outage.recovery_email_sent = True
|
||||
outage.save(update_fields=["recovery_email_sent"])
|
||||
def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
if not alert.sms_sent:
|
||||
sleep(random.randint(1, 15))
|
||||
alert.agent.send_outage_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send an sms only if the last sms sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.sms_sent < delta:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.agent.send_outage_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_outage_sms_task(pk):
|
||||
def agent_recovery_sms_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
sleep(random.randint(1, 3))
|
||||
outage = AgentOutage.objects.get(pk=pk)
|
||||
outage.send_outage_sms()
|
||||
outage.outage_sms_sent = True
|
||||
outage.save(update_fields=["outage_sms_sent"])
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
alert.agent.send_recovery_sms()
|
||||
alert.resolved_sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_recovery_sms_task(pk):
|
||||
sleep(random.randint(1, 3))
|
||||
outage = AgentOutage.objects.get(pk=pk)
|
||||
outage.send_recovery_sms()
|
||||
outage.recovery_sms_sent = True
|
||||
outage.save(update_fields=["recovery_sms_sent"])
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_outages_task():
|
||||
def agent_outages_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
|
||||
"pk",
|
||||
"last_seen",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
outages = AgentOutage.objects.filter(agent=agent)
|
||||
if outages and outages.last().is_active:
|
||||
continue
|
||||
|
||||
outage = AgentOutage(agent=agent)
|
||||
outage.save()
|
||||
|
||||
# add a null check history to allow gaps in graph
|
||||
for check in agent.agentchecks.all():
|
||||
check.add_check_history(None)
|
||||
|
||||
if agent.overdue_email_alert and not agent.maintenance_mode:
|
||||
agent_outage_email_task.delay(pk=outage.pk)
|
||||
|
||||
if agent.overdue_text_alert and not agent.maintenance_mode:
|
||||
agent_outage_sms_task.delay(pk=outage.pk)
|
||||
if agent.status == "overdue":
|
||||
agent.handle_alert()
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -264,12 +214,11 @@ def handle_agent_recovery_task(pk: int) -> None:
|
||||
|
||||
@app.task
|
||||
def run_script_email_results_task(
|
||||
agentpk: int, scriptpk: int, nats_timeout: int, nats_data: dict, emails: List[str]
|
||||
agentpk: int, scriptpk: int, nats_timeout: int, emails: List[str]
|
||||
):
|
||||
agent = Agent.objects.get(pk=agentpk)
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
nats_data["func"] = "runscriptfull"
|
||||
r = asyncio.run(agent.nats_cmd(nats_data, timeout=nats_timeout))
|
||||
r = agent.run_script(scriptpk=script.pk, full=True, timeout=nats_timeout, wait=True)
|
||||
if r == "timeout":
|
||||
logger.error(f"{agent.hostname} timed out running script.")
|
||||
return
|
||||
@@ -309,18 +258,3 @@ def run_script_email_results_task(
|
||||
server.quit()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
@app.task
|
||||
def remove_salt_task() -> None:
|
||||
if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT:
|
||||
return
|
||||
|
||||
q = Agent.objects.only("pk", "version")
|
||||
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")]
|
||||
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
|
||||
for chunk in chunks:
|
||||
for agent in chunk:
|
||||
asyncio.run(agent.nats_cmd({"func": "removesalt"}, wait=False))
|
||||
sleep(0.1)
|
||||
sleep(4)
|
||||
|
||||
@@ -4,16 +4,19 @@ from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker
|
||||
from itertools import cycle
|
||||
from typing import List
|
||||
from packaging import version as pyver
|
||||
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from logs.models import PendingAction
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from .serializers import AgentSerializer
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from .models import Agent
|
||||
from .tasks import auto_self_agent_update_task
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
|
||||
@@ -64,12 +67,34 @@ class TestAgentViews(TacticalTestCase):
|
||||
@patch("agents.tasks.send_agent_update_task.delay")
|
||||
def test_update_agents(self, mock_task):
|
||||
url = "/agents/updateagents/"
|
||||
data = {"pks": [1, 2, 3, 5, 10], "version": "0.11.1"}
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
_quantity=15,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.3.0",
|
||||
_quantity=15,
|
||||
)
|
||||
|
||||
pks: List[int] = list(
|
||||
Agent.objects.only("pk", "version").values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
data = {"pks": pks}
|
||||
expected: List[int] = [
|
||||
i.pk
|
||||
for i in Agent.objects.only("pk", "version")
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
mock_task.assert_called_with(pks=data["pks"], version=data["version"])
|
||||
mock_task.assert_called_with(pks=expected)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@@ -162,18 +187,44 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_get_event_log(self, mock_ret):
|
||||
url = f"/agents/{self.agent.pk}/geteventlog/Application/30/"
|
||||
def test_get_event_log(self, nats_cmd):
|
||||
url = f"/agents/{self.agent.pk}/geteventlog/Application/22/"
|
||||
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
|
||||
) as f:
|
||||
mock_ret.return_value = json.load(f)
|
||||
nats_cmd.return_value = json.load(f)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "eventlog",
|
||||
"timeout": 30,
|
||||
"payload": {
|
||||
"logname": "Application",
|
||||
"days": str(22),
|
||||
},
|
||||
},
|
||||
timeout=32,
|
||||
)
|
||||
|
||||
mock_ret.return_value = "timeout"
|
||||
url = f"/agents/{self.agent.pk}/geteventlog/Security/6/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "eventlog",
|
||||
"timeout": 180,
|
||||
"payload": {
|
||||
"logname": "Security",
|
||||
"days": str(6),
|
||||
},
|
||||
},
|
||||
timeout=182,
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@@ -308,7 +359,6 @@ class TestAgentViews(TacticalTestCase):
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertIn("rdp", r.json()["cmd"])
|
||||
self.assertNotIn("power", r.json()["cmd"])
|
||||
self.assertNotIn("ping", r.json()["cmd"])
|
||||
|
||||
data.update({"ping": 1, "power": 1})
|
||||
r = self.client.post(url, data, format="json")
|
||||
@@ -381,6 +431,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
"site": site.id,
|
||||
"monitoring_type": "workstation",
|
||||
"description": "asjdk234andasd",
|
||||
"offline_time": 4,
|
||||
"overdue_time": 300,
|
||||
"check_interval": 60,
|
||||
"overdue_email_alert": True,
|
||||
@@ -479,42 +530,20 @@ class TestAgentViews(TacticalTestCase):
|
||||
def test_overdue_action(self):
|
||||
url = "/agents/overdueaction/"
|
||||
|
||||
payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"}
|
||||
payload = {"pk": self.agent.pk, "overdue_email_alert": True}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
agent = Agent.objects.get(pk=self.agent.pk)
|
||||
self.assertTrue(agent.overdue_email_alert)
|
||||
self.assertEqual(self.agent.hostname, r.data)
|
||||
|
||||
payload.update({"alertType": "email", "action": "disabled"})
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
agent = Agent.objects.get(pk=self.agent.pk)
|
||||
self.assertFalse(agent.overdue_email_alert)
|
||||
self.assertEqual(self.agent.hostname, r.data)
|
||||
|
||||
payload.update({"alertType": "text", "action": "enabled"})
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
agent = Agent.objects.get(pk=self.agent.pk)
|
||||
self.assertTrue(agent.overdue_text_alert)
|
||||
self.assertEqual(self.agent.hostname, r.data)
|
||||
|
||||
payload.update({"alertType": "text", "action": "disabled"})
|
||||
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
agent = Agent.objects.get(pk=self.agent.pk)
|
||||
self.assertFalse(agent.overdue_text_alert)
|
||||
self.assertEqual(self.agent.hostname, r.data)
|
||||
|
||||
payload.update({"alertType": "email", "action": "523423"})
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
payload.update({"alertType": "text", "action": "asdasd3434asdasd"})
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_list_agents_no_detail(self):
|
||||
@@ -676,6 +705,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
class TestAgentViewsNew(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_agent_counts(self):
|
||||
url = "/agents/agent_counts/"
|
||||
@@ -686,15 +716,12 @@ class TestAgentViewsNew(TacticalTestCase):
|
||||
monitoring_type=cycle(["server", "workstation"]),
|
||||
_quantity=6,
|
||||
)
|
||||
agents = baker.make_recipe(
|
||||
baker.make_recipe(
|
||||
"agents.overdue_agent",
|
||||
monitoring_type=cycle(["server", "workstation"]),
|
||||
_quantity=6,
|
||||
)
|
||||
|
||||
# make an AgentOutage for every overdue agent
|
||||
baker.make("agents.AgentOutage", agent=cycle(agents), _quantity=6)
|
||||
|
||||
# returned data should be this
|
||||
data = {
|
||||
"total_server_count": 6,
|
||||
@@ -758,26 +785,28 @@ class TestAgentTasks(TacticalTestCase):
|
||||
agent_noarch = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Error getting OS",
|
||||
version="1.1.11",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
)
|
||||
r = agent_update(agent_noarch.pk)
|
||||
self.assertEqual(r, "noarch")
|
||||
self.assertEqual(
|
||||
PendingAction.objects.filter(
|
||||
agent=agent_noarch, action_type="agentupdate"
|
||||
).count(),
|
||||
0,
|
||||
)
|
||||
|
||||
agent64_111 = baker.make_recipe(
|
||||
agent_1111 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.1.11",
|
||||
)
|
||||
r = agent_update(agent_1111.pk)
|
||||
self.assertEqual(r, "not supported")
|
||||
|
||||
r = agent_update(agent64_111.pk)
|
||||
agent64_1112 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.1.12",
|
||||
)
|
||||
|
||||
r = agent_update(agent64_1112.pk)
|
||||
self.assertEqual(r, "created")
|
||||
action = PendingAction.objects.get(agent__pk=agent64_111.pk)
|
||||
action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
self.assertEqual(
|
||||
@@ -786,6 +815,17 @@ class TestAgentTasks(TacticalTestCase):
|
||||
)
|
||||
self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
|
||||
self.assertEqual(action.details["version"], "1.3.0")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
|
||||
"version": "1.3.0",
|
||||
"inno": "winagent-v1.3.0.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
|
||||
agent_64_130 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
@@ -806,128 +846,34 @@ class TestAgentTasks(TacticalTestCase):
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
agent64_old = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.2.1",
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
r = agent_update(agent64_old.pk)
|
||||
self.assertEqual(r, "created")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
|
||||
"version": "1.3.0",
|
||||
"inno": "winagent-v1.3.0.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
|
||||
""" @patch("agents.models.Agent.salt_api_async")
|
||||
@patch("agents.tasks.agent_update")
|
||||
@patch("agents.tasks.sleep", return_value=None)
|
||||
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
|
||||
# test 64bit golang agent
|
||||
self.agent64 = baker.make_recipe(
|
||||
def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.0.0",
|
||||
version=settings.LATEST_AGENT_VER,
|
||||
_quantity=23,
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
"url": settings.DL_64,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.agent64.delete()
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test 32bit golang agent
|
||||
self.agent32 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
|
||||
version="1.0.0",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
|
||||
"url": settings.DL_32,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.agent32.delete()
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test agent that has a null os field
|
||||
self.agentNone = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system=None,
|
||||
version="1.0.0",
|
||||
)
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_not_called()
|
||||
self.agentNone.delete()
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test auto update disabled in global settings
|
||||
self.agent64 = baker.make_recipe(
|
||||
baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.0.0",
|
||||
version="1.3.0",
|
||||
_quantity=33,
|
||||
)
|
||||
|
||||
self.coresettings.agent_auto_update = False
|
||||
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_not_called()
|
||||
|
||||
# reset core settings
|
||||
self.agent64.delete()
|
||||
salt_api_async.reset_mock()
|
||||
r = auto_self_agent_update_task.s().apply()
|
||||
self.assertEqual(agent_update.call_count, 0)
|
||||
|
||||
self.coresettings.agent_auto_update = True
|
||||
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||
|
||||
# test 64bit python agent
|
||||
self.agent64py = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="0.11.1",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": "winagent-v0.11.2.exe",
|
||||
"url": OLD_64_PY_AGENT,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.agent64py.delete()
|
||||
salt_api_async.reset_mock()
|
||||
|
||||
# test 32bit python agent
|
||||
self.agent32py = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
|
||||
version="0.11.1",
|
||||
)
|
||||
salt_api_async.return_value = True
|
||||
ret = auto_self_agent_update_task.s().apply()
|
||||
salt_api_async.assert_called_with(
|
||||
func="win_agent.do_agent_update_v2",
|
||||
kwargs={
|
||||
"inno": "winagent-v0.11.2-x86.exe",
|
||||
"url": OLD_32_PY_AGENT,
|
||||
},
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS") """
|
||||
r = auto_self_agent_update_task.s().apply()
|
||||
self.assertEqual(agent_update.call_count, 33)
|
||||
|
||||
@@ -18,7 +18,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, generics
|
||||
|
||||
from .models import Agent, AgentOutage, RecoveryAction, Note
|
||||
from .models import Agent, RecoveryAction, Note
|
||||
from core.models import CoreSettings
|
||||
from scripts.models import Script
|
||||
from logs.models import AuditLog, PendingAction
|
||||
@@ -30,6 +30,7 @@ from .serializers import (
|
||||
AgentEditSerializer,
|
||||
NoteSerializer,
|
||||
NotesSerializer,
|
||||
AgentOverdueActionSerializer,
|
||||
)
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
@@ -58,9 +59,13 @@ def get_agent_versions(request):
|
||||
|
||||
@api_view(["POST"])
|
||||
def update_agents(request):
|
||||
pks = request.data["pks"]
|
||||
version = request.data["version"]
|
||||
send_agent_update_task.delay(pks=pks, version=version)
|
||||
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
||||
pks: List[int] = [
|
||||
i.pk
|
||||
for i in q
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
send_agent_update_task.delay(pks=pks)
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@@ -92,22 +97,17 @@ def uninstall(request):
|
||||
def edit_agent(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||
|
||||
old_site = agent.site.pk
|
||||
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
a_serializer.is_valid(raise_exception=True)
|
||||
a_serializer.save()
|
||||
|
||||
policy = agent.winupdatepolicy.get()
|
||||
p_serializer = WinUpdatePolicySerializer(
|
||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||
)
|
||||
p_serializer.is_valid(raise_exception=True)
|
||||
p_serializer.save()
|
||||
|
||||
# check if site changed and initiate generating correct policies
|
||||
if old_site != request.data["site"]:
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
if "winupdatepolicy" in request.data.keys():
|
||||
policy = agent.winupdatepolicy.get()
|
||||
p_serializer = WinUpdatePolicySerializer(
|
||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||
)
|
||||
p_serializer.is_valid(raise_exception=True)
|
||||
p_serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -183,15 +183,16 @@ def get_event_log(request, pk, logtype, days):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
data = {
|
||||
"func": "eventlog",
|
||||
"timeout": 30,
|
||||
"timeout": timeout,
|
||||
"payload": {
|
||||
"logname": logtype,
|
||||
"days": str(days),
|
||||
},
|
||||
}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=32))
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
||||
if r == "timeout":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
@@ -242,6 +243,7 @@ class AgentsTableList(generics.ListAPIView):
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
@@ -290,6 +292,7 @@ def by_client(request, clientpk):
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
@@ -319,6 +322,7 @@ def by_site(request, sitepk):
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
@@ -333,26 +337,12 @@ def by_site(request, sitepk):
|
||||
|
||||
@api_view(["POST"])
|
||||
def overdue_action(request):
|
||||
pk = request.data["pk"]
|
||||
alert_type = request.data["alertType"]
|
||||
action = request.data["action"]
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
if alert_type == "email" and action == "enabled":
|
||||
agent.overdue_email_alert = True
|
||||
agent.save(update_fields=["overdue_email_alert"])
|
||||
elif alert_type == "email" and action == "disabled":
|
||||
agent.overdue_email_alert = False
|
||||
agent.save(update_fields=["overdue_email_alert"])
|
||||
elif alert_type == "text" and action == "enabled":
|
||||
agent.overdue_text_alert = True
|
||||
agent.save(update_fields=["overdue_text_alert"])
|
||||
elif alert_type == "text" and action == "disabled":
|
||||
agent.overdue_text_alert = False
|
||||
agent.save(update_fields=["overdue_text_alert"])
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
serializer = AgentOverdueActionSerializer(
|
||||
instance=agent, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(agent.hostname)
|
||||
|
||||
|
||||
@@ -473,7 +463,7 @@ def install_agent(request):
|
||||
f"GOARCH={goarch}",
|
||||
go_bin,
|
||||
"build",
|
||||
f"-ldflags=\"-X 'main.Inno={inno}'",
|
||||
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
|
||||
f"-X 'main.Api={api}'",
|
||||
f"-X 'main.Client={client_id}'",
|
||||
f"-X 'main.Site={site_id}'",
|
||||
@@ -571,12 +561,10 @@ def install_agent(request):
|
||||
"/VERYSILENT",
|
||||
"/SUPPRESSMSGBOXES",
|
||||
"&&",
|
||||
"timeout",
|
||||
"/t",
|
||||
"10",
|
||||
"/nobreak",
|
||||
">",
|
||||
"NUL",
|
||||
"ping",
|
||||
"127.0.0.1",
|
||||
"-n",
|
||||
"5",
|
||||
"&&",
|
||||
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
|
||||
"-m",
|
||||
@@ -714,19 +702,10 @@ def run_script(request):
|
||||
script=script.name,
|
||||
)
|
||||
|
||||
data = {
|
||||
"func": "runscript",
|
||||
"timeout": request.data["timeout"],
|
||||
"script_args": request.data["args"],
|
||||
"payload": {
|
||||
"code": script.code,
|
||||
"shell": script.shell,
|
||||
},
|
||||
}
|
||||
|
||||
if output == "wait":
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout))
|
||||
r = agent.run_script(scriptpk=script.pk, timeout=req_timeout, wait=True)
|
||||
return Response(r)
|
||||
|
||||
elif output == "email":
|
||||
if not pyver.parse(agent.version) >= pyver.parse("1.1.12"):
|
||||
return notify_error("Requires agent version 1.1.12 or greater")
|
||||
@@ -738,13 +717,12 @@ def run_script(request):
|
||||
agentpk=agent.pk,
|
||||
scriptpk=script.pk,
|
||||
nats_timeout=req_timeout,
|
||||
nats_data=data,
|
||||
emails=emails,
|
||||
)
|
||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||
else:
|
||||
asyncio.run(agent.nats_cmd(data, wait=False))
|
||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||
agent.run_script(scriptpk=script.pk, timeout=req_timeout)
|
||||
|
||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view()
|
||||
@@ -865,20 +843,43 @@ def bulk(request):
|
||||
|
||||
@api_view(["POST"])
|
||||
def agent_counts(request):
|
||||
|
||||
server_offline_count = len(
|
||||
[
|
||||
agent
|
||||
for agent in Agent.objects.filter(monitoring_type="server").only(
|
||||
"pk",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
if not agent.status == "online"
|
||||
]
|
||||
)
|
||||
|
||||
workstation_offline_count = len(
|
||||
[
|
||||
agent
|
||||
for agent in Agent.objects.filter(monitoring_type="workstation").only(
|
||||
"pk",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
if not agent.status == "online"
|
||||
]
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"total_server_count": Agent.objects.filter(
|
||||
monitoring_type="server"
|
||||
).count(),
|
||||
"total_server_offline_count": AgentOutage.objects.filter(
|
||||
recovery_time=None, agent__monitoring_type="server"
|
||||
).count(),
|
||||
"total_server_offline_count": server_offline_count,
|
||||
"total_workstation_count": Agent.objects.filter(
|
||||
monitoring_type="workstation"
|
||||
).count(),
|
||||
"total_workstation_offline_count": AgentOutage.objects.filter(
|
||||
recovery_time=None, agent__monitoring_type="workstation"
|
||||
).count(),
|
||||
"total_workstation_offline_count": workstation_offline_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Alert
|
||||
from .models import Alert, AlertTemplate
|
||||
|
||||
|
||||
admin.site.register(Alert)
|
||||
admin.site.register(AlertTemplate)
|
||||
|
||||
@@ -42,4 +42,4 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -27,4 +27,4 @@ class Migration(migrations.Migration):
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -28,4 +28,4 @@ class Migration(migrations.Migration):
|
||||
name="alert_time",
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
]
|
||||
]
|
||||
172
api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
Normal file
172
api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
Normal file
@@ -0,0 +1,172 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 14:08
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0029_delete_agentoutage'),
|
||||
('clients', '0008_auto_20201103_1430'),
|
||||
('autotasks', '0017_auto_20210210_1512'),
|
||||
('scripts', '0005_auto_20201207_1606'),
|
||||
('alerts', '0003_auto_20201021_1815'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_execution_time',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_retcode',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_run',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_stderr',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_stdout',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='action_timeout',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='alert_type',
|
||||
field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='assigned_task',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='email_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='hidden',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_execution_time',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_retcode',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_run',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_stderr',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_stdout',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_action_timeout',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_email_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_on',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='resolved_sms_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='sms_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alert',
|
||||
name='snoozed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='severity',
|
||||
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AlertTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||
('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||
('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
|
||||
('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
|
||||
('email_from', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||
('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('check_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('check_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('check_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||
('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
|
||||
('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('task_always_email', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('task_always_text', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('task_always_alert', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
|
||||
('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')),
|
||||
('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')),
|
||||
('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')),
|
||||
('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')),
|
||||
('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')),
|
||||
],
|
||||
),
|
||||
]
|
||||
31
api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
Normal file
31
api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 17:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0004_auto_20210212_1408'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='alert',
|
||||
name='action_timeout',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='alert',
|
||||
name='resolved_action_timeout',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alerttemplate',
|
||||
name='action_timeout',
|
||||
field=models.PositiveIntegerField(default=15),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alerttemplate',
|
||||
name='resolved_action_timeout',
|
||||
field=models.PositiveIntegerField(default=15),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.db import models
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.fields import BooleanField, PositiveIntegerField
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
SEVERITY_CHOICES = [
|
||||
("info", "Informational"),
|
||||
@@ -7,6 +9,13 @@ SEVERITY_CHOICES = [
|
||||
("error", "Error"),
|
||||
]
|
||||
|
||||
ALERT_TYPE_CHOICES = [
|
||||
("availability", "Availability"),
|
||||
("check", "Check"),
|
||||
("task", "Task"),
|
||||
("custom", "Custom"),
|
||||
]
|
||||
|
||||
|
||||
class Alert(models.Model):
|
||||
agent = models.ForeignKey(
|
||||
@@ -23,21 +32,253 @@ class Alert(models.Model):
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
assigned_task = models.ForeignKey(
|
||||
"autotasks.AutomatedTask",
|
||||
related_name="alert",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
alert_type = models.CharField(
|
||||
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
|
||||
)
|
||||
message = models.TextField(null=True, blank=True)
|
||||
alert_time = models.DateTimeField(auto_now_add=True, null=True)
|
||||
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
snoozed = models.BooleanField(default=False)
|
||||
snooze_until = models.DateTimeField(null=True, blank=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
severity = models.CharField(
|
||||
max_length=100, choices=SEVERITY_CHOICES, default="info"
|
||||
resolved_on = models.DateTimeField(null=True, blank=True)
|
||||
severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
|
||||
email_sent = models.DateTimeField(null=True, blank=True)
|
||||
resolved_email_sent = models.DateTimeField(null=True, blank=True)
|
||||
sms_sent = models.DateTimeField(null=True, blank=True)
|
||||
resolved_sms_sent = models.DateTimeField(null=True, blank=True)
|
||||
hidden = models.BooleanField(default=False)
|
||||
action_run = models.DateTimeField(null=True, blank=True)
|
||||
action_stdout = models.TextField(null=True, blank=True)
|
||||
action_stderr = models.TextField(null=True, blank=True)
|
||||
action_retcode = models.IntegerField(null=True, blank=True)
|
||||
action_execution_time = models.CharField(max_length=100, null=True, blank=True)
|
||||
resolved_action_run = models.DateTimeField(null=True, blank=True)
|
||||
resolved_action_stdout = models.TextField(null=True, blank=True)
|
||||
resolved_action_stderr = models.TextField(null=True, blank=True)
|
||||
resolved_action_retcode = models.IntegerField(null=True, blank=True)
|
||||
resolved_action_execution_time = models.CharField(
|
||||
max_length=100, null=True, blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def resolve(self):
|
||||
self.resolved = True
|
||||
self.resolved_on = djangotime.now()
|
||||
self.snoozed = False
|
||||
self.snooze_until = None
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def create_availability_alert(cls, agent):
|
||||
pass
|
||||
if not cls.objects.filter(agent=agent, resolved=False).exists():
|
||||
return cls.objects.create(
|
||||
agent=agent,
|
||||
alert_type="availability",
|
||||
severity="error",
|
||||
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is Offline.",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_check_alert(cls, check):
|
||||
|
||||
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
|
||||
return cls.objects.create(
|
||||
assigned_check=check,
|
||||
alert_type="check",
|
||||
severity=check.alert_severity,
|
||||
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_task_alert(cls, task):
|
||||
|
||||
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
|
||||
return cls.objects.create(
|
||||
assigned_task=task,
|
||||
alert_type="task",
|
||||
severity=task.alert_severity,
|
||||
message=f"{task.agent.hostname} has task: {task.name} that failed.",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_custom_alert(cls, custom):
|
||||
pass
|
||||
|
||||
|
||||
class AlertTemplate(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
action = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
related_name="alert_template",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
action_args = ArrayField(
|
||||
models.CharField(max_length=255, null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
action_timeout = models.PositiveIntegerField(default=15)
|
||||
resolved_action = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
related_name="resolved_alert_template",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
resolved_action_args = ArrayField(
|
||||
models.CharField(max_length=255, null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
resolved_action_timeout = models.PositiveIntegerField(default=15)
|
||||
|
||||
# overrides the global recipients
|
||||
email_recipients = ArrayField(
|
||||
models.CharField(max_length=100, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
text_recipients = ArrayField(
|
||||
models.CharField(max_length=100, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
|
||||
# overrides the from address
|
||||
email_from = models.EmailField(blank=True, null=True)
|
||||
|
||||
# agent alert settings
|
||||
agent_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
agent_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
agent_include_desktops = BooleanField(null=True, blank=True, default=False)
|
||||
agent_always_email = BooleanField(null=True, blank=True, default=False)
|
||||
agent_always_text = BooleanField(null=True, blank=True, default=False)
|
||||
agent_always_alert = BooleanField(null=True, blank=True, default=False)
|
||||
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
# check alert settings
|
||||
check_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
check_always_email = BooleanField(null=True, blank=True, default=False)
|
||||
check_always_text = BooleanField(null=True, blank=True, default=False)
|
||||
check_always_alert = BooleanField(null=True, blank=True, default=False)
|
||||
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
# task alert settings
|
||||
task_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
task_always_email = BooleanField(null=True, blank=True, default=False)
|
||||
task_always_text = BooleanField(null=True, blank=True, default=False)
|
||||
task_always_alert = BooleanField(null=True, blank=True, default=False)
|
||||
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
excluded_sites = models.ManyToManyField(
|
||||
"clients.Site", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
excluded_clients = models.ManyToManyField(
|
||||
"clients.Client", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
excluded_agents = models.ManyToManyField(
|
||||
"agents.Agent", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def has_agent_settings(self) -> bool:
|
||||
return (
|
||||
self.agent_email_on_resolved
|
||||
or self.agent_text_on_resolved
|
||||
or self.agent_include_desktops
|
||||
or self.agent_always_email
|
||||
or self.agent_always_text
|
||||
or self.agent_always_alert
|
||||
or bool(self.agent_periodic_alert_days)
|
||||
)
|
||||
|
||||
@property
|
||||
def has_check_settings(self) -> bool:
|
||||
return (
|
||||
bool(self.check_email_alert_severity)
|
||||
or bool(self.check_text_alert_severity)
|
||||
or bool(self.check_dashboard_alert_severity)
|
||||
or self.check_email_on_resolved
|
||||
or self.check_text_on_resolved
|
||||
or self.check_always_email
|
||||
or self.check_always_text
|
||||
or self.check_always_alert
|
||||
or bool(self.check_periodic_alert_days)
|
||||
)
|
||||
|
||||
@property
|
||||
def has_task_settings(self) -> bool:
|
||||
return (
|
||||
bool(self.task_email_alert_severity)
|
||||
or bool(self.task_text_alert_severity)
|
||||
or bool(self.task_dashboard_alert_severity)
|
||||
or self.task_email_on_resolved
|
||||
or self.task_text_on_resolved
|
||||
or self.task_always_email
|
||||
or self.task_always_text
|
||||
or self.task_always_alert
|
||||
or bool(self.task_periodic_alert_days)
|
||||
)
|
||||
|
||||
@property
|
||||
def has_core_settings(self) -> bool:
|
||||
return bool(self.email_from) or self.email_recipients or self.text_recipients
|
||||
|
||||
@property
|
||||
def is_default_template(self) -> bool:
|
||||
return self.default_alert_template.exists()
|
||||
|
||||
@@ -1,19 +1,124 @@
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
ReadOnlyField,
|
||||
DateTimeField,
|
||||
)
|
||||
|
||||
from .models import Alert
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from automation.serializers import PolicySerializer
|
||||
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
from .models import Alert, AlertTemplate
|
||||
|
||||
|
||||
class AlertSerializer(ModelSerializer):
|
||||
|
||||
hostname = ReadOnlyField(source="agent.hostname")
|
||||
client = ReadOnlyField(source="agent.client")
|
||||
site = ReadOnlyField(source="agent.site")
|
||||
alert_time = DateTimeField(format="iso-8601")
|
||||
hostname = SerializerMethodField(read_only=True)
|
||||
client = SerializerMethodField(read_only=True)
|
||||
site = SerializerMethodField(read_only=True)
|
||||
alert_time = SerializerMethodField(read_only=True)
|
||||
resolve_on = SerializerMethodField(read_only=True)
|
||||
snoozed_until = SerializerMethodField(read_only=True)
|
||||
|
||||
def get_hostname(self, instance):
|
||||
if instance.alert_type == "availability":
|
||||
return instance.agent.hostname if instance.agent else ""
|
||||
elif instance.alert_type == "check":
|
||||
return (
|
||||
instance.assigned_check.agent.hostname
|
||||
if instance.assigned_check
|
||||
else ""
|
||||
)
|
||||
elif instance.alert_type == "task":
|
||||
return (
|
||||
instance.assigned_task.agent.hostname if instance.assigned_task else ""
|
||||
)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_client(self, instance):
|
||||
if instance.alert_type == "availability":
|
||||
return instance.agent.client.name if instance.agent else ""
|
||||
elif instance.alert_type == "check":
|
||||
return (
|
||||
instance.assigned_check.agent.client.name
|
||||
if instance.assigned_check
|
||||
else ""
|
||||
)
|
||||
elif instance.alert_type == "task":
|
||||
return (
|
||||
instance.assigned_task.agent.client.name
|
||||
if instance.assigned_task
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_site(self, instance):
|
||||
if instance.alert_type == "availability":
|
||||
return instance.agent.site.name if instance.agent else ""
|
||||
elif instance.alert_type == "check":
|
||||
return (
|
||||
instance.assigned_check.agent.site.name
|
||||
if instance.assigned_check
|
||||
else ""
|
||||
)
|
||||
elif instance.alert_type == "task":
|
||||
return (
|
||||
instance.assigned_task.agent.site.name if instance.assigned_task else ""
|
||||
)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_alert_time(self, instance):
|
||||
if instance.alert_time:
|
||||
return instance.alert_time.astimezone(get_default_timezone()).timestamp()
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_resolve_on(self, instance):
|
||||
if instance.resolved_on:
|
||||
return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_snoozed_until(self, instance):
|
||||
if instance.snooze_until:
|
||||
return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = Alert
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AlertTemplateSerializer(ModelSerializer):
|
||||
agent_settings = ReadOnlyField(source="has_agent_settings")
|
||||
check_settings = ReadOnlyField(source="has_check_settings")
|
||||
task_settings = ReadOnlyField(source="has_task_settings")
|
||||
core_settings = ReadOnlyField(source="has_core_settings")
|
||||
default_template = ReadOnlyField(source="is_default_template")
|
||||
action_name = ReadOnlyField(source="action.name")
|
||||
resolved_action_name = ReadOnlyField(source="resolved_action.name")
|
||||
applied_count = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AlertTemplate
|
||||
fields = "__all__"
|
||||
|
||||
def get_applied_count(self, instance):
|
||||
count = 0
|
||||
count += instance.policies.count()
|
||||
count += instance.clients.count()
|
||||
count += instance.sites.count()
|
||||
return count
|
||||
|
||||
|
||||
class AlertTemplateRelationSerializer(ModelSerializer):
|
||||
policies = PolicySerializer(read_only=True, many=True)
|
||||
clients = ClientSerializer(read_only=True, many=True)
|
||||
sites = SiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = AlertTemplate
|
||||
fields = "__all__"
|
||||
|
||||
15
api/tacticalrmm/alerts/tasks.py
Normal file
15
api/tacticalrmm/alerts/tasks.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
from alerts.models import Alert
|
||||
|
||||
|
||||
@app.task
|
||||
def unsnooze_alerts() -> str:
|
||||
|
||||
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
|
||||
snoozed=False, snooze_until=None
|
||||
)
|
||||
|
||||
return "ok"
|
||||
@@ -1,3 +1,377 @@
|
||||
from django.test import TestCase
|
||||
from datetime import datetime, timedelta
|
||||
from core.models import CoreSettings
|
||||
|
||||
# Create your tests here.
|
||||
from django.utils import timezone as djangotime
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
AlertTemplateSerializer,
|
||||
AlertTemplateRelationSerializer,
|
||||
)
|
||||
|
||||
|
||||
class TestAlertsViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_get_alerts(self):
|
||||
url = "/alerts/alerts/"
|
||||
|
||||
# create check, task, and agent to test each serializer function
|
||||
check = baker.make_recipe("checks.diskspace_check")
|
||||
task = baker.make("autotasks.AutomatedTask")
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
# setup data
|
||||
alerts = baker.make(
|
||||
"alerts.Alert",
|
||||
agent=agent,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
severity="warning",
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
assigned_check=check,
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
severity="error",
|
||||
_quantity=7,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
assigned_task=task,
|
||||
snoozed=True,
|
||||
snooze_until=djangotime.now(),
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
_quantity=2,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
agent=agent,
|
||||
resolved=True,
|
||||
resolved_on=djangotime.now(),
|
||||
alert_time=seq(datetime.now(), timedelta(days=15)),
|
||||
_quantity=9,
|
||||
)
|
||||
|
||||
# test top alerts for alerts icon
|
||||
data = {"top": 3}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
|
||||
self.assertEquals(resp.data["alerts_count"], 10)
|
||||
|
||||
# test filter data
|
||||
# test data and result counts
|
||||
data = [
|
||||
{
|
||||
"filter": {
|
||||
"timeFilter": 30,
|
||||
"snoozedFilter": True,
|
||||
"resolvedFilter": False,
|
||||
},
|
||||
"count": 12,
|
||||
},
|
||||
{
|
||||
"filter": {
|
||||
"timeFilter": 45,
|
||||
"snoozedFilter": False,
|
||||
"resolvedFilter": False,
|
||||
},
|
||||
"count": 10,
|
||||
},
|
||||
{
|
||||
"filter": {
|
||||
"severityFilter": ["error"],
|
||||
"snoozedFilter": False,
|
||||
"resolvedFilter": True,
|
||||
"timeFilter": 20,
|
||||
},
|
||||
"count": 7,
|
||||
},
|
||||
{
|
||||
"filter": {
|
||||
"clientFilter": [],
|
||||
"snoozedFilter": True,
|
||||
"resolvedFilter": False,
|
||||
},
|
||||
"count": 0,
|
||||
},
|
||||
{"filter": {}, "count": 21},
|
||||
{"filter": {"snoozedFilter": True, "resolvedFilter": False}, "count": 12},
|
||||
]
|
||||
|
||||
for req in data:
|
||||
resp = self.client.patch(url, req["filter"], format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), req["count"])
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_add_alert(self):
|
||||
url = "/alerts/alerts/"
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
data = {
|
||||
"alert_time": datetime.now(),
|
||||
"agent": agent.id,
|
||||
"severity": "warning",
|
||||
"alert_type": "availability",
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_get_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.get("/alerts/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert")
|
||||
url = f"/alerts/alerts/{alert.pk}/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertSerializer(alert)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_update_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
|
||||
|
||||
url = f"/alerts/alerts/{alert.pk}/"
|
||||
|
||||
# test resolving alert
|
||||
data = {
|
||||
"id": alert.pk,
|
||||
"type": "resolve",
|
||||
}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved)
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on)
|
||||
|
||||
# test snoozing alert
|
||||
data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed)
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until)
|
||||
|
||||
# test snoozing alert without snooze_days
|
||||
data = {"id": alert.pk, "type": "snooze"}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# test unsnoozing alert
|
||||
data = {"id": alert.pk, "type": "unsnooze"}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed)
|
||||
self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until)
|
||||
|
||||
# test invalid type
|
||||
data = {"id": alert.pk, "type": "invalid"}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert")
|
||||
|
||||
# test delete alert
|
||||
url = f"/alerts/alerts/{alert.pk}/"
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertFalse(Alert.objects.filter(pk=alert.pk).exists())
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_bulk_alert_actions(self):
|
||||
url = "/alerts/bulk/"
|
||||
|
||||
# setup data
|
||||
alerts = baker.make("alerts.Alert", resolved=False, _quantity=3)
|
||||
|
||||
# test invalid data
|
||||
data = {"bulk_action": "invalid"}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# test snooze without snooze days
|
||||
data = {"bulk_action": "snooze"}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# test bulk snoozing alerts
|
||||
data = {
|
||||
"bulk_action": "snooze",
|
||||
"alerts": [alert.pk for alert in alerts],
|
||||
"snooze_days": "30",
|
||||
}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(Alert.objects.filter(snoozed=False).exists())
|
||||
|
||||
# test bulk resolving alerts
|
||||
data = {"bulk_action": "resolve", "alerts": [alert.pk for alert in alerts]}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(Alert.objects.filter(resolved=False).exists())
|
||||
self.assertTrue(Alert.objects.filter(snoozed=False).exists())
|
||||
|
||||
def test_get_alert_templates(self):
|
||||
url = "/alerts/alerttemplates/"
|
||||
|
||||
alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertTemplateSerializer(alert_templates, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_alert_template(self):
|
||||
url = "/alerts/alerttemplates/"
|
||||
|
||||
data = {
|
||||
"name": "Test Template",
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_get_alert_template(self):
|
||||
# returns 404 for invalid alert template pk
|
||||
resp = self.client.get("/alerts/alerttemplates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertTemplateSerializer(alert_template)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_update_alert_template(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/"
|
||||
|
||||
# test data
|
||||
data = {
|
||||
"id": alert_template.pk,
|
||||
"agent_email_on_resolved": True,
|
||||
"agent_text_on_resolved": True,
|
||||
"agent_include_desktops": True,
|
||||
"agent_always_email": True,
|
||||
"agent_always_text": True,
|
||||
"agent_always_alert": True,
|
||||
"agent_periodic_alert_days": "90",
|
||||
}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_alert_template(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
|
||||
# test delete alert
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/"
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertFalse(AlertTemplate.objects.filter(pk=alert_template.pk).exists())
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_alert_template_related(self):
|
||||
# setup data
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
baker.make("clients.Client", alert_template=alert_template, _quantity=2)
|
||||
baker.make("clients.Site", alert_template=alert_template, _quantity=3)
|
||||
baker.make("automation.Policy", alert_template=alert_template)
|
||||
core = CoreSettings.objects.first()
|
||||
core.alert_template = alert_template
|
||||
core.save()
|
||||
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/related/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertTemplateRelationSerializer(alert_template)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
self.assertEqual(len(resp.data["policies"]), 1)
|
||||
self.assertEqual(len(resp.data["clients"]), 2)
|
||||
self.assertEqual(len(resp.data["sites"]), 3)
|
||||
self.assertTrue(
|
||||
AlertTemplate.objects.get(pk=alert_template.pk).is_default_template
|
||||
)
|
||||
|
||||
|
||||
class TestAlertTasks(TacticalTestCase):
|
||||
def test_unsnooze_alert_task(self):
|
||||
from alerts.tasks import unsnooze_alerts
|
||||
|
||||
# these will be unsnoozed whent eh function is run
|
||||
not_snoozed = baker.make(
|
||||
"alerts.Alert",
|
||||
snoozed=True,
|
||||
snooze_until=seq(datetime.now(), timedelta(days=15)),
|
||||
_quantity=5,
|
||||
)
|
||||
|
||||
# these will still be snoozed after the function is run
|
||||
snoozed = baker.make(
|
||||
"alerts.Alert",
|
||||
snoozed=True,
|
||||
snooze_until=seq(datetime.now(), timedelta(days=-15)),
|
||||
_quantity=5,
|
||||
)
|
||||
|
||||
unsnooze_alerts()
|
||||
|
||||
self.assertFalse(
|
||||
Alert.objects.filter(
|
||||
pk__in=[alert.pk for alert in not_snoozed], snoozed=False
|
||||
).exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
Alert.objects.filter(
|
||||
pk__in=[alert.pk for alert in snoozed], snoozed=False
|
||||
).exists()
|
||||
)
|
||||
|
||||
@@ -3,5 +3,9 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("alerts/", views.GetAddAlerts.as_view()),
|
||||
path("bulk/", views.BulkAlerts.as_view()),
|
||||
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
|
||||
path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
|
||||
path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
|
||||
path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,19 +1,103 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
from datetime import datetime as dt
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from .models import Alert
|
||||
from .models import Alert, AlertTemplate
|
||||
|
||||
from .serializers import AlertSerializer
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
AlertTemplateSerializer,
|
||||
AlertTemplateRelationSerializer,
|
||||
)
|
||||
|
||||
|
||||
class GetAddAlerts(APIView):
|
||||
def get(self, request):
|
||||
alerts = Alert.objects.all()
|
||||
def patch(self, request):
|
||||
|
||||
return Response(AlertSerializer(alerts, many=True).data)
|
||||
# top 10 alerts for dashboard icon
|
||||
if "top" in request.data.keys():
|
||||
alerts = Alert.objects.filter(
|
||||
resolved=False, snoozed=False, hidden=False
|
||||
).order_by("alert_time")[: int(request.data["top"])]
|
||||
count = Alert.objects.filter(
|
||||
resolved=False, snoozed=False, hidden=False
|
||||
).count()
|
||||
return Response(
|
||||
{
|
||||
"alerts_count": count,
|
||||
"alerts": AlertSerializer(alerts, many=True).data,
|
||||
}
|
||||
)
|
||||
|
||||
elif any(
|
||||
key
|
||||
in [
|
||||
"timeFilter",
|
||||
"clientFilter",
|
||||
"severityFilter",
|
||||
"resolvedFilter",
|
||||
"snoozedFilter",
|
||||
]
|
||||
for key in request.data.keys()
|
||||
):
|
||||
clientFilter = Q()
|
||||
severityFilter = Q()
|
||||
timeFilter = Q()
|
||||
resolvedFilter = Q()
|
||||
snoozedFilter = Q()
|
||||
|
||||
if (
|
||||
"snoozedFilter" in request.data.keys()
|
||||
and not request.data["snoozedFilter"]
|
||||
):
|
||||
snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
|
||||
|
||||
if (
|
||||
"resolvedFilter" in request.data.keys()
|
||||
and not request.data["resolvedFilter"]
|
||||
):
|
||||
resolvedFilter = Q(resolved=request.data["resolvedFilter"])
|
||||
|
||||
if "clientFilter" in request.data.keys():
|
||||
from agents.models import Agent
|
||||
from clients.models import Client
|
||||
|
||||
clients = Client.objects.filter(
|
||||
pk__in=request.data["clientFilter"]
|
||||
).values_list("id")
|
||||
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
|
||||
"id"
|
||||
)
|
||||
|
||||
clientFilter = Q(agent__in=agents)
|
||||
|
||||
if "severityFilter" in request.data.keys():
|
||||
severityFilter = Q(severity__in=request.data["severityFilter"])
|
||||
|
||||
if "timeFilter" in request.data.keys():
|
||||
timeFilter = Q(
|
||||
alert_time__lte=djangotime.make_aware(dt.today()),
|
||||
alert_time__gt=djangotime.make_aware(dt.today())
|
||||
- djangotime.timedelta(days=int(request.data["timeFilter"])),
|
||||
)
|
||||
|
||||
alerts = (
|
||||
Alert.objects.filter(clientFilter)
|
||||
.filter(severityFilter)
|
||||
.filter(resolvedFilter)
|
||||
.filter(snoozedFilter)
|
||||
.filter(timeFilter)
|
||||
)
|
||||
return Response(AlertSerializer(alerts, many=True).data)
|
||||
|
||||
else:
|
||||
alerts = Alert.objects.all()
|
||||
return Response(AlertSerializer(alerts, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = AlertSerializer(data=request.data, partial=True)
|
||||
@@ -32,7 +116,40 @@ class GetUpdateDeleteAlert(APIView):
|
||||
def put(self, request, pk):
|
||||
alert = get_object_or_404(Alert, pk=pk)
|
||||
|
||||
serializer = AlertSerializer(instance=alert, data=request.data, partial=True)
|
||||
data = request.data
|
||||
|
||||
if "type" in data.keys():
|
||||
if data["type"] == "resolve":
|
||||
data = {
|
||||
"resolved": True,
|
||||
"resolved_on": djangotime.now(),
|
||||
"snoozed": False,
|
||||
}
|
||||
|
||||
# unable to set snooze_until to none in serialzier
|
||||
alert.snooze_until = None
|
||||
alert.save()
|
||||
elif data["type"] == "snooze":
|
||||
if "snooze_days" in data.keys():
|
||||
data = {
|
||||
"snoozed": True,
|
||||
"snooze_until": djangotime.now()
|
||||
+ djangotime.timedelta(days=int(data["snooze_days"])),
|
||||
}
|
||||
else:
|
||||
return notify_error(
|
||||
"Missing 'snoozed_days' when trying to snooze alert"
|
||||
)
|
||||
elif data["type"] == "unsnooze":
|
||||
data = {"snoozed": False}
|
||||
|
||||
# unable to set snooze_until to none in serialzier
|
||||
alert.snooze_until = None
|
||||
alert.save()
|
||||
else:
|
||||
return notify_error("There was an error in the request data")
|
||||
|
||||
serializer = AlertSerializer(instance=alert, data=data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
@@ -42,3 +159,68 @@ class GetUpdateDeleteAlert(APIView):
|
||||
Alert.objects.get(pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class BulkAlerts(APIView):
|
||||
def post(self, request):
|
||||
if request.data["bulk_action"] == "resolve":
|
||||
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||
resolved=True,
|
||||
resolved_on=djangotime.now(),
|
||||
snoozed=False,
|
||||
snooze_until=None,
|
||||
)
|
||||
return Response("ok")
|
||||
elif request.data["bulk_action"] == "snooze":
|
||||
if "snooze_days" in request.data.keys():
|
||||
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||
snoozed=True,
|
||||
snooze_until=djangotime.now()
|
||||
+ djangotime.timedelta(days=int(request.data["snooze_days"])),
|
||||
)
|
||||
return Response("ok")
|
||||
|
||||
return notify_error("The request was invalid")
|
||||
|
||||
|
||||
class GetAddAlertTemplates(APIView):
|
||||
def get(self, request):
|
||||
alert_templates = AlertTemplate.objects.all()
|
||||
|
||||
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = AlertTemplateSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class GetUpdateDeleteAlertTemplate(APIView):
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
|
||||
return Response(AlertTemplateSerializer(alert_template).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
|
||||
serializer = AlertTemplateSerializer(
|
||||
instance=alert_template, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(AlertTemplate, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class RelatedAlertTemplate(APIView):
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
return Response(AlertTemplateRelationSerializer(alert_template).data)
|
||||
|
||||
@@ -26,21 +26,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_mesh_info(self):
|
||||
url = f"/api/v3/{self.agent.pk}/meshinfo/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_winupdater(self):
|
||||
url = f"/api/v3/{self.agent.agent_id}/winupdater/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_sysinfo(self):
|
||||
# TODO replace this with golang wmi sample data
|
||||
|
||||
@@ -59,23 +44,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_hello_patch(self):
|
||||
url = "/api/v3/hello/"
|
||||
payload = {
|
||||
"agent_id": self.agent.agent_id,
|
||||
"logged_in_username": "None",
|
||||
"disks": [],
|
||||
}
|
||||
|
||||
r = self.client.patch(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
payload["logged_in_username"] = "Bob"
|
||||
r = self.client.patch(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_checkrunner_interval(self):
|
||||
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
|
||||
r = self.client.get(url, format="json")
|
||||
|
||||
@@ -2,18 +2,18 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("checkin/", views.CheckIn.as_view()),
|
||||
path("hello/", views.Hello.as_view()),
|
||||
path("checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
|
||||
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
|
||||
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
|
||||
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
|
||||
path("meshexe/", views.MeshExe.as_view()),
|
||||
path("sysinfo/", views.SysInfo.as_view()),
|
||||
path("newagent/", views.NewAgent.as_view()),
|
||||
path("winupdater/", views.WinUpdater.as_view()),
|
||||
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
|
||||
path("software/", views.Software.as_view()),
|
||||
path("installer/", views.Installer.as_view()),
|
||||
path("checkin/", views.CheckIn.as_view()),
|
||||
path("syncmesh/", views.SyncMeshNodeID.as_view()),
|
||||
path("choco/", views.Choco.as_view()),
|
||||
path("winupdates/", views.WinUpdates.as_view()),
|
||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
|
||||
@@ -17,68 +17,58 @@ from rest_framework.authtoken.models import Token
|
||||
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
from checks.utils import bytes2human
|
||||
from autotasks.models import AutomatedTask
|
||||
from accounts.models import User
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
from software.models import InstalledSoftware
|
||||
from checks.serializers import CheckRunnerGetSerializerV3
|
||||
from agents.serializers import WinAgentSerializer
|
||||
from checks.serializers import CheckRunnerGetSerializer
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
|
||||
from winupdate.serializers import ApprovedUpdateSerializer
|
||||
from agents.serializers import WinAgentSerializer
|
||||
|
||||
from agents.tasks import (
|
||||
agent_recovery_email_task,
|
||||
agent_recovery_sms_task,
|
||||
)
|
||||
from checks.utils import bytes2human
|
||||
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class CheckIn(APIView):
|
||||
"""
|
||||
The agent's checkin endpoint
|
||||
patch: called every 45 to 110 seconds, handles agent updates and recovery
|
||||
put: called every 5 to 10 minutes, handles basic system info
|
||||
post: called once on windows service startup
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request):
|
||||
updated = False
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if pyver.parse(request.data["version"]) > pyver.parse(
|
||||
agent.version
|
||||
) or pyver.parse(request.data["version"]) == pyver.parse(
|
||||
settings.LATEST_AGENT_VER
|
||||
):
|
||||
updated = True
|
||||
agent.version = request.data["version"]
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["version", "last_seen"])
|
||||
|
||||
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
|
||||
last_outage = agent.agentoutages.last()
|
||||
last_outage.recovery_time = djangotime.now()
|
||||
last_outage.save(update_fields=["recovery_time"])
|
||||
# change agent update pending status to completed if agent has just updated
|
||||
if (
|
||||
updated
|
||||
and agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists()
|
||||
):
|
||||
agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).update(status="completed")
|
||||
|
||||
if agent.overdue_email_alert:
|
||||
agent_recovery_email_task.delay(pk=last_outage.pk)
|
||||
if agent.overdue_text_alert:
|
||||
agent_recovery_sms_task.delay(pk=last_outage.pk)
|
||||
# handles any alerting actions
|
||||
agent.handle_alert(checkin=True)
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last()
|
||||
if recovery is not None:
|
||||
recovery.last_run = djangotime.now()
|
||||
recovery.save(update_fields=["last_run"])
|
||||
return Response(recovery.send())
|
||||
|
||||
# handle agent update
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
update = agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).last()
|
||||
update.status = "completed"
|
||||
update.save(update_fields=["status"])
|
||||
return Response(update.details)
|
||||
handle_agent_recovery_task.delay(pk=recovery.pk)
|
||||
return Response("ok")
|
||||
|
||||
# get any pending actions
|
||||
if agent.pendingactions.filter(status="pending").exists():
|
||||
@@ -89,75 +79,13 @@ class CheckIn(APIView):
|
||||
def put(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if "disks" in request.data.keys():
|
||||
if request.data["func"] == "disks":
|
||||
disks = request.data["disks"]
|
||||
new = []
|
||||
# python agent
|
||||
if isinstance(disks, dict):
|
||||
for k, v in disks.items():
|
||||
new.append(v)
|
||||
else:
|
||||
# golang agent
|
||||
for disk in disks:
|
||||
tmp = {}
|
||||
for k, v in disk.items():
|
||||
tmp["device"] = disk["device"]
|
||||
tmp["fstype"] = disk["fstype"]
|
||||
tmp["total"] = bytes2human(disk["total"])
|
||||
tmp["used"] = bytes2human(disk["used"])
|
||||
tmp["free"] = bytes2human(disk["free"])
|
||||
tmp["percent"] = int(disk["percent"])
|
||||
new.append(tmp)
|
||||
|
||||
serializer.save(disks=new)
|
||||
return Response("ok")
|
||||
|
||||
if "logged_in_username" in request.data.keys():
|
||||
if request.data["logged_in_username"] != "None":
|
||||
serializer.save(last_logged_in_user=request.data["logged_in_username"])
|
||||
return Response("ok")
|
||||
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_seen=djangotime.now())
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class Hello(APIView):
|
||||
#### DEPRECATED, for agents <= 1.1.9 ####
|
||||
"""
|
||||
The agent's checkin endpoint
|
||||
patch: called every 30 to 120 seconds
|
||||
post: called on agent windows service startup
|
||||
"""
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
disks = request.data["disks"]
|
||||
new = []
|
||||
# python agent
|
||||
if isinstance(disks, dict):
|
||||
for k, v in disks.items():
|
||||
new.append(v)
|
||||
else:
|
||||
# golang agent
|
||||
for disk in disks:
|
||||
tmp = {}
|
||||
for k, v in disk.items():
|
||||
for _, _ in disk.items():
|
||||
tmp["device"] = disk["device"]
|
||||
tmp["fstype"] = disk["fstype"]
|
||||
tmp["total"] = bytes2human(disk["total"])
|
||||
@@ -166,54 +94,174 @@ class Hello(APIView):
|
||||
tmp["percent"] = int(disk["percent"])
|
||||
new.append(tmp)
|
||||
|
||||
if request.data["logged_in_username"] == "None":
|
||||
serializer.save(last_seen=djangotime.now(), disks=new)
|
||||
else:
|
||||
serializer.save(
|
||||
last_seen=djangotime.now(),
|
||||
disks=new,
|
||||
last_logged_in_user=request.data["logged_in_username"],
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(disks=new)
|
||||
return Response("ok")
|
||||
|
||||
if request.data["func"] == "loggedonuser":
|
||||
if request.data["logged_in_username"] != "None":
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_logged_in_user=request.data["logged_in_username"])
|
||||
return Response("ok")
|
||||
|
||||
if request.data["func"] == "software":
|
||||
raw: SoftwareList = request.data["software"]
|
||||
if not isinstance(raw, list):
|
||||
return notify_error("err")
|
||||
|
||||
sw = filter_software(raw)
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
else:
|
||||
s = agent.installedsoftware_set.first()
|
||||
s.software = sw
|
||||
s.save(update_fields=["software"])
|
||||
|
||||
return Response("ok")
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
# called once during tacticalagent windows service startup
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if not agent.choco_installed:
|
||||
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
|
||||
|
||||
time.sleep(0.5)
|
||||
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SyncMeshNodeID(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if agent.mesh_node_id != request.data["nodeid"]:
|
||||
agent.mesh_node_id = request.data["nodeid"]
|
||||
agent.save(update_fields=["mesh_node_id"])
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class Choco(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
agent.choco_installed = request.data["installed"]
|
||||
agent.save(update_fields=["choco_installed"])
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class WinUpdates(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def put(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
reboot_policy: str = agent.get_patch_policy().reboot_after_install
|
||||
reboot = False
|
||||
|
||||
if reboot_policy == "always":
|
||||
reboot = True
|
||||
|
||||
if request.data["needs_reboot"]:
|
||||
if reboot_policy == "required":
|
||||
reboot = True
|
||||
elif reboot_policy == "never":
|
||||
agent.needs_reboot = True
|
||||
agent.save(update_fields=["needs_reboot"])
|
||||
|
||||
if reboot:
|
||||
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
|
||||
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
u = agent.winupdates.filter(guid=request.data["guid"]).last()
|
||||
success: bool = request.data["success"]
|
||||
if success:
|
||||
u.result = "success"
|
||||
u.downloaded = True
|
||||
u.installed = True
|
||||
u.date_installed = djangotime.now()
|
||||
u.save(
|
||||
update_fields=[
|
||||
"result",
|
||||
"downloaded",
|
||||
"installed",
|
||||
"date_installed",
|
||||
]
|
||||
)
|
||||
else:
|
||||
u.result = "failed"
|
||||
u.save(update_fields=["result"])
|
||||
|
||||
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
|
||||
last_outage = agent.agentoutages.last()
|
||||
last_outage.recovery_time = djangotime.now()
|
||||
last_outage.save(update_fields=["recovery_time"])
|
||||
|
||||
if agent.overdue_email_alert:
|
||||
agent_recovery_email_task.delay(pk=last_outage.pk)
|
||||
if agent.overdue_text_alert:
|
||||
agent_recovery_sms_task.delay(pk=last_outage.pk)
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last()
|
||||
if recovery is not None:
|
||||
recovery.last_run = djangotime.now()
|
||||
recovery.save(update_fields=["last_run"])
|
||||
return Response(recovery.send())
|
||||
|
||||
# handle agent update
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
update = agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).last()
|
||||
update.status = "completed"
|
||||
update.save(update_fields=["status"])
|
||||
return Response(update.details)
|
||||
|
||||
# get any pending actions
|
||||
if agent.pendingactions.filter(status="pending").exists():
|
||||
agent.handle_pending_actions()
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
updates = request.data["wua_updates"]
|
||||
for update in updates:
|
||||
if agent.winupdates.filter(guid=update["guid"]).exists():
|
||||
u = agent.winupdates.filter(guid=update["guid"]).last()
|
||||
u.downloaded = update["downloaded"]
|
||||
u.installed = update["installed"]
|
||||
u.save(update_fields=["downloaded", "installed"])
|
||||
else:
|
||||
try:
|
||||
kb = "KB" + update["kb_article_ids"][0]
|
||||
except:
|
||||
continue
|
||||
|
||||
WinUpdate(
|
||||
agent=agent,
|
||||
guid=update["guid"],
|
||||
kb=kb,
|
||||
title=update["title"],
|
||||
installed=update["installed"],
|
||||
downloaded=update["downloaded"],
|
||||
description=update["description"],
|
||||
severity=update["severity"],
|
||||
categories=update["categories"],
|
||||
category_ids=update["category_ids"],
|
||||
kb_article_ids=update["kb_article_ids"],
|
||||
more_info_urls=update["more_info_urls"],
|
||||
support_url=update["support_url"],
|
||||
revision_number=update["revision_number"],
|
||||
).save()
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
|
||||
# more superseded updates cleanup
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
|
||||
for u in agent.winupdates.filter(
|
||||
date_installed__isnull=True, result="failed"
|
||||
).exclude(installed=True):
|
||||
u.delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SupersededWinUpdate(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
updates = agent.winupdates.filter(guid=request.data["guid"])
|
||||
for u in updates:
|
||||
u.delete()
|
||||
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_seen=djangotime.now())
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@@ -232,7 +280,7 @@ class CheckRunner(APIView):
|
||||
ret = {
|
||||
"agent": agent.pk,
|
||||
"check_interval": agent.check_interval,
|
||||
"checks": CheckRunnerGetSerializerV3(checks, many=True).data,
|
||||
"checks": CheckRunnerGetSerializer(checks, many=True).data,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
@@ -280,6 +328,8 @@ class TaskRunner(APIView):
|
||||
serializer.save(last_run=djangotime.now())
|
||||
|
||||
new_task = AutomatedTask.objects.get(pk=task.pk)
|
||||
new_task.handle_alert()
|
||||
|
||||
AuditLog.objects.create(
|
||||
username=agent.hostname,
|
||||
agent=agent.hostname,
|
||||
@@ -292,74 +342,6 @@ class TaskRunner(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class WinUpdater(APIView):
|
||||
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent.delete_superseded_updates()
|
||||
patches = agent.winupdates.filter(action="approve").exclude(installed=True)
|
||||
return Response(ApprovedUpdateSerializer(patches, many=True).data)
|
||||
|
||||
# agent sends patch results as it's installing them
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
kb = request.data["kb"]
|
||||
results = request.data["results"]
|
||||
update = agent.winupdates.get(kb=kb)
|
||||
|
||||
if results == "error" or results == "failed":
|
||||
update.result = results
|
||||
update.save(update_fields=["result"])
|
||||
elif results == "success":
|
||||
update.result = "success"
|
||||
update.downloaded = True
|
||||
update.installed = True
|
||||
update.date_installed = djangotime.now()
|
||||
update.save(
|
||||
update_fields=[
|
||||
"result",
|
||||
"downloaded",
|
||||
"installed",
|
||||
"date_installed",
|
||||
]
|
||||
)
|
||||
elif results == "alreadyinstalled":
|
||||
update.result = "success"
|
||||
update.downloaded = True
|
||||
update.installed = True
|
||||
update.save(update_fields=["result", "downloaded", "installed"])
|
||||
|
||||
return Response("ok")
|
||||
|
||||
# agent calls this after it's finished installing all patches
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
reboot_policy = agent.get_patch_policy().reboot_after_install
|
||||
reboot = False
|
||||
|
||||
if reboot_policy == "always":
|
||||
reboot = True
|
||||
|
||||
if request.data["reboot"]:
|
||||
if reboot_policy == "required":
|
||||
reboot = True
|
||||
elif reboot_policy == "never":
|
||||
agent.needs_reboot = True
|
||||
agent.save(update_fields=["needs_reboot"])
|
||||
|
||||
if reboot:
|
||||
if agent.has_nats:
|
||||
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
|
||||
logger.info(
|
||||
f"{agent.hostname} is rebooting after updates were installed."
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SysInfo(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -375,29 +357,6 @@ class SysInfo(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class MeshInfo(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
return Response(agent.mesh_node_id)
|
||||
|
||||
def patch(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
|
||||
if "nodeidhex" in request.data:
|
||||
# agent <= 1.1.0
|
||||
nodeid = request.data["nodeidhex"]
|
||||
else:
|
||||
# agent >= 1.1.1
|
||||
nodeid = request.data["nodeid"]
|
||||
|
||||
agent.mesh_node_id = nodeid
|
||||
agent.save(update_fields=["mesh_node_id"])
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class MeshExe(APIView):
|
||||
""" Sends the mesh exe to the installer """
|
||||
|
||||
@@ -462,10 +421,6 @@ class NewAgent(APIView):
|
||||
|
||||
reload_nats()
|
||||
|
||||
# Generate policies for new agent
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
# create agent install audit record
|
||||
AuditLog.objects.create(
|
||||
username=request.user,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 14:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0004_auto_20210212_1408'),
|
||||
('automation', '0006_delete_policyexclusions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='policy',
|
||||
name='alert_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='policies', to='alerts.alerttemplate'),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,36 @@ class Policy(BaseAuditModel):
|
||||
desc = models.CharField(max_length=255, null=True, blank=True)
|
||||
active = models.BooleanField(default=False)
|
||||
enforced = models.BooleanField(default=False)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="policies",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from automation.tasks import generate_agent_checks_from_policies_task
|
||||
|
||||
# get old policy if exists
|
||||
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
# generate agent checks only if active and enforced were changed
|
||||
if old_policy:
|
||||
if old_policy.active != self.active or old_policy.enforced != self.enforced:
|
||||
generate_agent_checks_from_policies_task.delay(
|
||||
policypk=self.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
|
||||
super(BaseAuditModel, self).delete(*args, **kwargs)
|
||||
|
||||
generate_agent_checks_task.delay(agents, create_tasks=True)
|
||||
|
||||
@property
|
||||
def is_default_server_policy(self):
|
||||
@@ -122,7 +152,9 @@ class Policy(BaseAuditModel):
|
||||
delete_win_task_schedule.delay(task.pk)
|
||||
|
||||
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
|
||||
for action in agent.pendingactions.exclude(status="completed"):
|
||||
for action in agent.pendingactions.filter(action_type="taskaction").exclude(
|
||||
status="completed"
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=action.details["task_id"])
|
||||
if (
|
||||
task.parent_task in agent_tasks_parent_pks
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
from django.db.models.base import Model
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
SerializerMethodField,
|
||||
StringRelatedField,
|
||||
ReadOnlyField,
|
||||
)
|
||||
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
|
||||
from .models import Policy
|
||||
from agents.models import Agent
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from clients.models import Client, Site
|
||||
from clients.models import Client
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
|
||||
@@ -24,15 +20,11 @@ class PolicySerializer(ModelSerializer):
|
||||
|
||||
class PolicyTableSerializer(ModelSerializer):
|
||||
|
||||
server_clients = ClientSerializer(many=True, read_only=True)
|
||||
server_sites = SiteSerializer(many=True, read_only=True)
|
||||
workstation_clients = ClientSerializer(many=True, read_only=True)
|
||||
workstation_sites = SiteSerializer(many=True, read_only=True)
|
||||
agents = AgentHostnameSerializer(many=True, read_only=True)
|
||||
default_server_policy = ReadOnlyField(source="is_default_server_policy")
|
||||
default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
|
||||
agents_count = SerializerMethodField(read_only=True)
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
alert_template = ReadOnlyField(source="alert_template.id")
|
||||
|
||||
class Meta:
|
||||
model = Policy
|
||||
@@ -78,49 +70,16 @@ class PolicyCheckSerializer(ModelSerializer):
|
||||
"assignedtask",
|
||||
"text_alert",
|
||||
"email_alert",
|
||||
"dashboard_alert",
|
||||
)
|
||||
depth = 1
|
||||
|
||||
|
||||
class AutoTasksFieldSerializer(ModelSerializer):
|
||||
assigned_check = PolicyCheckSerializer(read_only=True)
|
||||
script = ReadOnlyField(source="script.id")
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
fields = ("id", "enabled", "name", "schedule", "assigned_check")
|
||||
depth = 1
|
||||
|
||||
|
||||
class AutoTaskPolicySerializer(ModelSerializer):
|
||||
|
||||
autotasks = AutoTasksFieldSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Policy
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"autotasks",
|
||||
)
|
||||
depth = 2
|
||||
|
||||
|
||||
class RelatedClientPolicySerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = ("workstation_policy", "server_policy")
|
||||
depth = 1
|
||||
|
||||
|
||||
class RelatedSitePolicySerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ("workstation_policy", "server_policy")
|
||||
depth = 1
|
||||
|
||||
|
||||
class RelatedAgentPolicySerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = ("policy",)
|
||||
fields = "__all__"
|
||||
depth = 1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from automation.models import Policy
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from agents.models import Agent
|
||||
|
||||
@@ -6,6 +7,7 @@ from tacticalrmm.celery import app
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on agents affected by a policy and optionally generate automated tasks
|
||||
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
|
||||
|
||||
policy = Policy.objects.get(pk=policypk)
|
||||
@@ -21,7 +23,7 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
else:
|
||||
agents = policy.related_agents()
|
||||
agents = policy.related_agents().only("pk")
|
||||
|
||||
for agent in agents:
|
||||
agent.generate_checks_from_policies()
|
||||
@@ -30,6 +32,17 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on a list of agents and optionally generate automated tasks
|
||||
def generate_agent_checks_task(agentpks, create_tasks=False):
|
||||
for agent in Agent.objects.filter(pk__in=agentpks):
|
||||
agent.generate_checks_from_policies()
|
||||
|
||||
if create_tasks:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
|
||||
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
|
||||
|
||||
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
|
||||
@@ -40,6 +53,7 @@ def generate_agent_checks_by_location_task(location, mon_type, create_tasks=Fals
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
|
||||
def generate_all_agent_checks_task(mon_type, create_tasks=False):
|
||||
for agent in Agent.objects.filter(monitoring_type=mon_type):
|
||||
agent.generate_checks_from_policies()
|
||||
@@ -49,22 +63,30 @@ def generate_all_agent_checks_task(mon_type, create_tasks=False):
|
||||
|
||||
|
||||
@app.task
|
||||
# deletes a policy managed check from all agents
|
||||
def delete_policy_check_task(checkpk):
|
||||
|
||||
Check.objects.filter(parent_check=checkpk).delete()
|
||||
|
||||
|
||||
@app.task
|
||||
# updates policy managed check fields on agents
|
||||
def update_policy_check_fields_task(checkpk):
|
||||
|
||||
check = Check.objects.get(pk=checkpk)
|
||||
|
||||
Check.objects.filter(parent_check=checkpk).update(
|
||||
threshold=check.threshold,
|
||||
warning_threshold=check.warning_threshold,
|
||||
error_threshold=check.error_threshold,
|
||||
alert_severity=check.alert_severity,
|
||||
name=check.name,
|
||||
disk=check.disk,
|
||||
fails_b4_alert=check.fails_b4_alert,
|
||||
ip=check.ip,
|
||||
script=check.script,
|
||||
script_args=check.script_args,
|
||||
info_return_codes=check.info_return_codes,
|
||||
warning_return_codes=check.warning_return_codes,
|
||||
timeout=check.timeout,
|
||||
pass_if_start_pending=check.pass_if_start_pending,
|
||||
pass_if_svc_not_exist=check.pass_if_svc_not_exist,
|
||||
@@ -79,10 +101,12 @@ def update_policy_check_fields_task(checkpk):
|
||||
search_last_days=check.search_last_days,
|
||||
email_alert=check.email_alert,
|
||||
text_alert=check.text_alert,
|
||||
dashboard_alert=check.dashboard_alert,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy tasks on agents affected by a policy
|
||||
def generate_agent_tasks_from_policies_task(policypk):
|
||||
|
||||
policy = Policy.objects.get(pk=policypk)
|
||||
@@ -98,19 +122,12 @@ def generate_agent_tasks_from_policies_task(policypk):
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
else:
|
||||
agents = policy.related_agents()
|
||||
agents = policy.related_agents().only("pk")
|
||||
|
||||
for agent in agents:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
def generate_agent_tasks_by_location_task(location, mon_type):
|
||||
|
||||
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
def delete_policy_autotask_task(taskpk):
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
@@ -129,13 +146,23 @@ def run_win_policy_autotask_task(task_pks):
|
||||
|
||||
|
||||
@app.task
|
||||
def update_policy_task_fields_task(taskpk, enabled):
|
||||
from autotasks.models import AutomatedTask
|
||||
def update_policy_task_fields_task(taskpk, update_agent=False):
|
||||
from autotasks.tasks import enable_or_disable_win_task
|
||||
|
||||
tasks = AutomatedTask.objects.filter(parent_task=taskpk)
|
||||
task = AutomatedTask.objects.get(pk=taskpk)
|
||||
|
||||
tasks.update(enabled=enabled)
|
||||
AutomatedTask.objects.filter(parent_task=taskpk).update(
|
||||
alert_severity=task.alert_severity,
|
||||
email_alert=task.email_alert,
|
||||
text_alert=task.text_alert,
|
||||
dashboard_alert=task.dashboard_alert,
|
||||
script=task.script,
|
||||
script_args=task.script_args,
|
||||
name=task.name,
|
||||
timeout=task.timeout,
|
||||
enabled=task.enabled,
|
||||
)
|
||||
|
||||
for autotask in tasks:
|
||||
enable_or_disable_win_task(autotask.pk, enabled)
|
||||
if update_agent:
|
||||
for task in AutomatedTask.objects.filter(parent_task=taskpk):
|
||||
enable_or_disable_win_task.delay(task.pk, task.enabled)
|
||||
|
||||
@@ -9,13 +9,10 @@ from .serializers import (
|
||||
PolicyTableSerializer,
|
||||
PolicySerializer,
|
||||
PolicyTaskStatusSerializer,
|
||||
AutoTaskPolicySerializer,
|
||||
PolicyOverviewSerializer,
|
||||
PolicyCheckStatusSerializer,
|
||||
PolicyCheckSerializer,
|
||||
RelatedAgentPolicySerializer,
|
||||
RelatedSitePolicySerializer,
|
||||
RelatedClientPolicySerializer,
|
||||
AutoTasksFieldSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -91,7 +88,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||
def test_update_policy(self, mock_checks_task):
|
||||
def test_update_policy(self, generate_agent_checks_from_policies_task):
|
||||
# returns 404 for invalid policy pk
|
||||
resp = self.client.put("/automation/policies/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -110,7 +107,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# only called if active or enforced are updated
|
||||
mock_checks_task.assert_not_called()
|
||||
generate_agent_checks_from_policies_task.assert_not_called()
|
||||
|
||||
data = {
|
||||
"name": "Test Policy Update",
|
||||
@@ -121,40 +118,43 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mock_checks_task.assert_called_with(policypk=policy.pk, create_tasks=True)
|
||||
generate_agent_checks_from_policies_task.assert_called_with(
|
||||
policypk=policy.pk, create_tasks=True
|
||||
)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
|
||||
def test_delete_policy(self, mock_tasks_task, mock_checks_task):
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_delete_policy(self, generate_agent_checks_task):
|
||||
# returns 404 for invalid policy pk
|
||||
resp = self.client.delete("/automation/policies/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
site = baker.make("clients.Site")
|
||||
agents = baker.make_recipe(
|
||||
"agents.agent", site=site, policy=policy, _quantity=3
|
||||
)
|
||||
url = f"/automation/policies/{policy.pk}/"
|
||||
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
mock_checks_task.assert_called_with(policypk=policy.pk)
|
||||
mock_tasks_task.assert_called_with(policypk=policy.pk)
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
[agent.pk for agent in agents], create_tasks=True
|
||||
)
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_all_policy_tasks(self):
|
||||
# returns 404 for invalid policy pk
|
||||
resp = self.client.get("/automation/500/policyautomatedtasks/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# create policy with tasks
|
||||
policy = baker.make("automation.Policy")
|
||||
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
url = f"/automation/{policy.pk}/policyautomatedtasks/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AutoTaskPolicySerializer(policy)
|
||||
serializer = AutoTasksFieldSerializer(tasks, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
@@ -180,8 +180,9 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_policy_check_status(self):
|
||||
# set data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.agent", site=site)
|
||||
policy = baker.make("automation.Policy")
|
||||
policy_diskcheck = baker.make_recipe("checks.diskspace_check", policy=policy)
|
||||
managed_check = baker.make_recipe(
|
||||
@@ -246,266 +247,6 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.generate_checks_from_policies")
|
||||
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
|
||||
def test_update_policy_add(
|
||||
self,
|
||||
mock_checks_location_task,
|
||||
mock_checks_task,
|
||||
):
|
||||
url = f"/automation/related/"
|
||||
|
||||
# data setup
|
||||
policy = baker.make("automation.Policy")
|
||||
client = baker.make("clients.Client")
|
||||
site = baker.make("clients.Site", client=client)
|
||||
agent = baker.make_recipe("agents.agent", site=site)
|
||||
|
||||
# test add client to policy data
|
||||
client_server_payload = {
|
||||
"type": "client",
|
||||
"pk": agent.client.pk,
|
||||
"server_policy": policy.pk,
|
||||
}
|
||||
client_workstation_payload = {
|
||||
"type": "client",
|
||||
"pk": agent.client.pk,
|
||||
"workstation_policy": policy.pk,
|
||||
}
|
||||
|
||||
# test add site to policy data
|
||||
site_server_payload = {
|
||||
"type": "site",
|
||||
"pk": agent.site.pk,
|
||||
"server_policy": policy.pk,
|
||||
}
|
||||
site_workstation_payload = {
|
||||
"type": "site",
|
||||
"pk": agent.site.pk,
|
||||
"workstation_policy": policy.pk,
|
||||
}
|
||||
|
||||
# test add agent to policy data
|
||||
agent_payload = {"type": "agent", "pk": agent.pk, "policy": policy.pk}
|
||||
|
||||
# test client server policy add
|
||||
resp = self.client.post(url, client_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test client workstation policy add
|
||||
resp = self.client.post(url, client_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test site add server policy
|
||||
resp = self.client.post(url, site_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site_id": site.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test site add workstation policy
|
||||
resp = self.client.post(url, site_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site_id": site.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test agent add
|
||||
resp = self.client.post(url, agent_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_task.assert_called()
|
||||
mock_checks_task.reset_mock()
|
||||
|
||||
# Adding the same relations shouldn't trigger mocks
|
||||
resp = self.client.post(url, client_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp = self.client.post(url, client_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mock_checks_location_task.assert_not_called()
|
||||
|
||||
resp = self.client.post(url, site_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp = self.client.post(url, site_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mock_checks_location_task.assert_not_called()
|
||||
|
||||
resp = self.client.post(url, agent_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_task.assert_not_called()
|
||||
|
||||
# test remove client from policy data
|
||||
client_server_payload = {"type": "client", "pk": client.pk, "server_policy": 0}
|
||||
client_workstation_payload = {
|
||||
"type": "client",
|
||||
"pk": client.pk,
|
||||
"workstation_policy": 0,
|
||||
}
|
||||
|
||||
# test remove site from policy data
|
||||
site_server_payload = {"type": "site", "pk": site.pk, "server_policy": 0}
|
||||
site_workstation_payload = {
|
||||
"type": "site",
|
||||
"pk": site.pk,
|
||||
"workstation_policy": 0,
|
||||
}
|
||||
|
||||
# test remove agent from policy
|
||||
agent_payload = {"type": "agent", "pk": agent.pk, "policy": 0}
|
||||
|
||||
# test client server policy remove
|
||||
resp = self.client.post(url, client_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test client workstation policy remove
|
||||
resp = self.client.post(url, client_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test site remove server policy
|
||||
resp = self.client.post(url, site_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site_id": site.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test site remove workstation policy
|
||||
resp = self.client.post(url, site_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# called because the relation changed
|
||||
mock_checks_location_task.assert_called_with(
|
||||
location={"site_id": site.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
mock_checks_location_task.reset_mock()
|
||||
|
||||
# test agent remove
|
||||
resp = self.client.post(url, agent_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# called because the relation changed
|
||||
mock_checks_task.assert_called()
|
||||
mock_checks_task.reset_mock()
|
||||
|
||||
# adding the same relations shouldn't trigger mocks
|
||||
resp = self.client.post(url, client_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(url, client_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# shouldn't be called since nothing changed
|
||||
mock_checks_location_task.assert_not_called()
|
||||
|
||||
resp = self.client.post(url, site_server_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(url, site_workstation_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# shouldn't be called since nothing changed
|
||||
mock_checks_location_task.assert_not_called()
|
||||
|
||||
resp = self.client.post(url, agent_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# shouldn't be called since nothing changed
|
||||
mock_checks_task.assert_not_called()
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_get_relation_by_type(self):
|
||||
url = f"/automation/related/"
|
||||
|
||||
# data setup
|
||||
policy = baker.make("automation.Policy")
|
||||
client = baker.make("clients.Client", workstation_policy=policy)
|
||||
site = baker.make("clients.Site", server_policy=policy)
|
||||
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
|
||||
|
||||
client_payload = {"type": "client", "pk": client.pk}
|
||||
|
||||
# test add site to policy
|
||||
site_payload = {"type": "site", "pk": site.pk}
|
||||
|
||||
# test add agent to policy
|
||||
agent_payload = {"type": "agent", "pk": agent.pk}
|
||||
|
||||
# test client relation get
|
||||
serializer = RelatedClientPolicySerializer(client)
|
||||
resp = self.client.patch(url, client_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
# test site relation get
|
||||
serializer = RelatedSitePolicySerializer(site)
|
||||
resp = self.client.patch(url, site_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
# test agent relation get
|
||||
serializer = RelatedAgentPolicySerializer(agent)
|
||||
resp = self.client.patch(url, agent_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
|
||||
invalid_payload = {"type": "bad_type", "pk": 5}
|
||||
|
||||
resp = self.client.patch(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_get_policy_task_status(self):
|
||||
|
||||
# policy with a task
|
||||
@@ -739,8 +480,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
checks = self.create_checks(policy=policy)
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
|
||||
agent = baker.make_recipe("agents.agent", policy=policy)
|
||||
|
||||
# test policy assigned to agent
|
||||
generate_agent_checks_from_policies_task(policy.id)
|
||||
@@ -756,16 +496,19 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
if check.check_type == "diskspace":
|
||||
self.assertEqual(check.parent_check, checks[0].id)
|
||||
self.assertEqual(check.disk, checks[0].disk)
|
||||
self.assertEqual(check.threshold, checks[0].threshold)
|
||||
self.assertEqual(check.error_threshold, checks[0].error_threshold)
|
||||
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
|
||||
elif check.check_type == "ping":
|
||||
self.assertEqual(check.parent_check, checks[1].id)
|
||||
self.assertEqual(check.ip, checks[1].ip)
|
||||
elif check.check_type == "cpuload":
|
||||
self.assertEqual(check.parent_check, checks[2].id)
|
||||
self.assertEqual(check.threshold, checks[2].threshold)
|
||||
self.assertEqual(check.error_threshold, checks[0].error_threshold)
|
||||
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
|
||||
elif check.check_type == "memory":
|
||||
self.assertEqual(check.parent_check, checks[3].id)
|
||||
self.assertEqual(check.threshold, checks[3].threshold)
|
||||
self.assertEqual(check.error_threshold, checks[0].error_threshold)
|
||||
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
|
||||
elif check.check_type == "winsvc":
|
||||
self.assertEqual(check.parent_check, checks[4].id)
|
||||
self.assertEqual(check.svc_name, checks[4].svc_name)
|
||||
@@ -801,69 +544,245 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
7,
|
||||
)
|
||||
|
||||
def test_generating_agent_policy_checks_by_location(self):
|
||||
from .tasks import generate_agent_checks_by_location_task
|
||||
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
|
||||
def test_generating_agent_policy_checks_by_location(
|
||||
self, generate_agent_checks_by_location_task
|
||||
):
|
||||
from automation.tasks import (
|
||||
generate_agent_checks_by_location_task as generate_agent_checks,
|
||||
)
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(policy=policy)
|
||||
clients = baker.make(
|
||||
"clients.Client",
|
||||
_quantity=2,
|
||||
server_policy=policy,
|
||||
workstation_policy=policy,
|
||||
)
|
||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
|
||||
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
|
||||
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
|
||||
agent1 = baker.make_recipe("agents.server_agent", site=sites[1])
|
||||
agent2 = baker.make_recipe("agents.workstation_agent", site=sites[3])
|
||||
|
||||
generate_agent_checks_by_location_task(
|
||||
{"site_id": sites[0].id},
|
||||
"server",
|
||||
baker.make(
|
||||
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
|
||||
)
|
||||
|
||||
server_agent = baker.make_recipe("agents.server_agent")
|
||||
workstation_agent = baker.make_recipe("agents.workstation_agent")
|
||||
|
||||
# no checks should be preset on agents
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
|
||||
# set workstation policy on client and policy checks should be there
|
||||
workstation_agent.client.workstation_policy = policy
|
||||
workstation_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# server_agent should have policy checks and the other agents should not
|
||||
# make sure the checks were added
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
|
||||
# remove workstation policy from client
|
||||
workstation_agent.client.workstation_policy = None
|
||||
workstation_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure the checks were removed
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
|
||||
# set server policy on client and policy checks should be there
|
||||
server_agent.client.server_policy = policy
|
||||
server_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were added
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
|
||||
|
||||
generate_agent_checks_by_location_task(
|
||||
{"site__client_id": clients[0].id},
|
||||
"workstation",
|
||||
# remove server policy from client
|
||||
server_agent.client.server_policy = None
|
||||
server_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
# workstation_agent should now have policy checks and the other agents should not
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were removed
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
|
||||
# set workstation policy on site and policy checks should be there
|
||||
workstation_agent.site.workstation_policy = policy
|
||||
workstation_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were added on workstation
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
|
||||
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
|
||||
self.assertEqual(Agent.objects.get(pk=agent2.id).agentchecks.count(), 0)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
|
||||
def test_generating_policy_checks_for_all_agents(self):
|
||||
from .tasks import generate_all_agent_checks_task
|
||||
# remove workstation policy from site
|
||||
workstation_agent.site.workstation_policy = None
|
||||
workstation_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were removed
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
|
||||
# set server policy on site and policy checks should be there
|
||||
server_agent.site.server_policy = policy
|
||||
server_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were added
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
|
||||
# remove server policy from site
|
||||
server_agent.site.server_policy = None
|
||||
server_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# make sure checks were removed
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_generating_policy_checks_for_all_agents(
|
||||
self, generate_all_agent_checks_task
|
||||
):
|
||||
from .tasks import generate_all_agent_checks_task as generate_all_checks
|
||||
from core.models import CoreSettings
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(policy=policy)
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
server_agents = baker.make_recipe("agents.server_agent", site=site, _quantity=3)
|
||||
workstation_agents = baker.make_recipe(
|
||||
"agents.workstation_agent", site=site, _quantity=4
|
||||
)
|
||||
server_agents = baker.make_recipe("agents.server_agent", _quantity=3)
|
||||
workstation_agents = baker.make_recipe("agents.workstation_agent", _quantity=4)
|
||||
core = CoreSettings.objects.first()
|
||||
core.server_policy = policy
|
||||
core.workstation_policy = policy
|
||||
core.save()
|
||||
|
||||
generate_all_agent_checks_task("server", create_tasks=True)
|
||||
generate_all_agent_checks_task.assert_called_with(
|
||||
mon_type="server", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="server", create_tasks=True)
|
||||
|
||||
# all servers should have 7 checks
|
||||
for agent in server_agents:
|
||||
@@ -872,24 +791,50 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
for agent in workstation_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
|
||||
|
||||
generate_all_agent_checks_task("workstation", create_tasks=True)
|
||||
core.server_policy = None
|
||||
core.workstation_policy = policy
|
||||
core.save()
|
||||
|
||||
# all agents should have 7 checks now
|
||||
generate_all_agent_checks_task.assert_any_call(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.assert_any_call(
|
||||
mon_type="server", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="server", create_tasks=True)
|
||||
generate_all_checks(mon_type="workstation", create_tasks=True)
|
||||
|
||||
# all workstations should have 7 checks
|
||||
for agent in server_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
|
||||
|
||||
for agent in workstation_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||
|
||||
core.workstation_policy = None
|
||||
core.save()
|
||||
|
||||
generate_all_agent_checks_task.assert_called_with(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="workstation", create_tasks=True)
|
||||
|
||||
# nothing should have the checks
|
||||
for agent in server_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
|
||||
|
||||
for agent in workstation_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
|
||||
|
||||
def test_delete_policy_check(self):
|
||||
from .tasks import delete_policy_check_task
|
||||
from .models import Policy
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(policy=policy)
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
|
||||
agent.generate_checks_from_policies()
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
# make sure agent has 7 checks
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||
@@ -914,7 +859,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(policy=policy)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
agent.generate_checks_from_policies()
|
||||
|
||||
# make sure agent has 7 checks
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||
@@ -946,8 +890,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
tasks = baker.make(
|
||||
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
|
||||
)
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
generate_agent_tasks_from_policies_task(policy.id)
|
||||
|
||||
@@ -968,61 +911,19 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.assertEqual(task.parent_task, tasks[2].id)
|
||||
self.assertEqual(task.name, tasks[2].name)
|
||||
|
||||
def test_generate_agent_tasks_by_location(self):
|
||||
from .tasks import generate_agent_tasks_by_location_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make(
|
||||
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
|
||||
)
|
||||
clients = baker.make(
|
||||
"clients.Client",
|
||||
_quantity=2,
|
||||
server_policy=policy,
|
||||
workstation_policy=policy,
|
||||
)
|
||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
|
||||
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
|
||||
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
|
||||
agent1 = baker.make_recipe("agents.agent", site=sites[1])
|
||||
agent2 = baker.make_recipe("agents.agent", site=sites[3])
|
||||
|
||||
generate_agent_tasks_by_location_task({"site_id": sites[0].id}, "server")
|
||||
|
||||
# all servers in site1 and site2 should have 3 tasks
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 0
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
|
||||
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
|
||||
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
|
||||
|
||||
generate_agent_tasks_by_location_task(
|
||||
{"site__client_id": clients[0].id}, "workstation"
|
||||
)
|
||||
|
||||
# all workstations in Default1 should have 3 tasks
|
||||
self.assertEqual(
|
||||
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 3
|
||||
)
|
||||
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
|
||||
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
|
||||
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
|
||||
|
||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||
def test_delete_policy_tasks(self, delete_win_task_schedule):
|
||||
from .tasks import delete_policy_autotask_task
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
|
||||
agent.generate_tasks_from_policies()
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
delete_policy_autotask_task(tasks[0].id)
|
||||
|
||||
delete_win_task_schedule.assert_called_with(agent.autotasks.first().id)
|
||||
delete_win_task_schedule.assert_called_with(
|
||||
agent.autotasks.get(parent_task=tasks[0].id).id
|
||||
)
|
||||
|
||||
@patch("autotasks.tasks.run_win_task.delay")
|
||||
def test_run_policy_task(self, run_win_task):
|
||||
@@ -1037,25 +938,46 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
for task in tasks:
|
||||
run_win_task.assert_any_call(task.id)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_update_policy_tasks(self, nats_cmd):
|
||||
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
|
||||
def test_update_policy_tasks(self, enable_or_disable_win_task):
|
||||
from .tasks import update_policy_task_fields_task
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make(
|
||||
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
|
||||
)
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
|
||||
agent.generate_tasks_from_policies()
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
tasks[0].enabled = False
|
||||
tasks[0].save()
|
||||
|
||||
update_policy_task_fields_task(tasks[0].id, enabled=False)
|
||||
update_policy_task_fields_task(tasks[0].id)
|
||||
enable_or_disable_win_task.assert_not_called()
|
||||
|
||||
self.assertFalse(AutomatedTask.objects.get(parent_task=tasks[0].id).enabled)
|
||||
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
|
||||
|
||||
update_policy_task_fields_task(tasks[0].id, update_agent=True)
|
||||
enable_or_disable_win_task.assert_called_with(
|
||||
agent.autotasks.get(parent_task=tasks[0].id).id, False
|
||||
)
|
||||
|
||||
@patch("agents.models.Agent.generate_tasks_from_policies")
|
||||
@patch("agents.models.Agent.generate_checks_from_policies")
|
||||
def test_generate_agent_checks_with_agentpks(self, generate_checks, generate_tasks):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
agents = baker.make_recipe("agents.agent", _quantity=5)
|
||||
|
||||
# reset because creating agents triggers it
|
||||
generate_checks.reset_mock()
|
||||
generate_tasks.reset_mock()
|
||||
|
||||
generate_agent_checks_task([agent.pk for agent in agents])
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
generate_tasks.assert_not_called()
|
||||
generate_checks.reset_mock()
|
||||
|
||||
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
|
||||
@@ -4,7 +4,6 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("policies/", views.GetAddPolicies.as_view()),
|
||||
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
|
||||
path("related/", views.GetRelated.as_view()),
|
||||
path("policies/overview/", views.OverviewPolicy.as_view()),
|
||||
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
|
||||
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
|
||||
|
||||
@@ -2,11 +2,10 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from .models import Policy
|
||||
from agents.models import Agent
|
||||
from clients.models import Client, Site
|
||||
from clients.models import Client
|
||||
from checks.models import Check
|
||||
from autotasks.models import AutomatedTask
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
@@ -22,16 +21,10 @@ from .serializers import (
|
||||
PolicyCheckStatusSerializer,
|
||||
PolicyCheckSerializer,
|
||||
PolicyTaskStatusSerializer,
|
||||
AutoTaskPolicySerializer,
|
||||
RelatedClientPolicySerializer,
|
||||
RelatedSitePolicySerializer,
|
||||
RelatedAgentPolicySerializer,
|
||||
AutoTasksFieldSerializer,
|
||||
)
|
||||
|
||||
from .tasks import (
|
||||
generate_agent_checks_from_policies_task,
|
||||
generate_agent_checks_by_location_task,
|
||||
generate_agent_tasks_from_policies_task,
|
||||
run_win_policy_autotask_task,
|
||||
)
|
||||
|
||||
@@ -72,29 +65,14 @@ class GetUpdateDeletePolicy(APIView):
|
||||
def put(self, request, pk):
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
old_active = policy.active
|
||||
old_enforced = policy.enforced
|
||||
|
||||
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
saved_policy = serializer.save()
|
||||
|
||||
# Generate agent checks only if active and enforced were changed
|
||||
if saved_policy.active != old_active or saved_policy.enforced != old_enforced:
|
||||
generate_agent_checks_from_policies_task.delay(
|
||||
policypk=policy.pk,
|
||||
create_tasks=(saved_policy.active != old_active),
|
||||
)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
# delete all managed policy checks off of agents
|
||||
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
|
||||
generate_agent_tasks_from_policies_task.delay(policypk=policy.pk)
|
||||
policy.delete()
|
||||
get_object_or_404(Policy, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -103,8 +81,8 @@ class PolicyAutoTask(APIView):
|
||||
|
||||
# tasks associated with policy
|
||||
def get(self, request, pk):
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
return Response(AutoTaskPolicySerializer(policy).data)
|
||||
tasks = AutomatedTask.objects.filter(policy=pk)
|
||||
return Response(AutoTasksFieldSerializer(tasks, many=True).data)
|
||||
|
||||
# get status of all tasks
|
||||
def patch(self, request, task):
|
||||
@@ -183,205 +161,12 @@ class GetRelated(APIView):
|
||||
).data
|
||||
|
||||
response["agents"] = AgentHostnameSerializer(
|
||||
policy.related_agents(),
|
||||
policy.related_agents().only("pk", "hostname"),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
return Response(response)
|
||||
|
||||
# update agents, clients, sites to policy
|
||||
def post(self, request):
|
||||
|
||||
related_type = request.data["type"]
|
||||
pk = request.data["pk"]
|
||||
|
||||
# workstation policy is set
|
||||
if (
|
||||
"workstation_policy" in request.data
|
||||
and request.data["workstation_policy"] != 0
|
||||
):
|
||||
policy = get_object_or_404(Policy, pk=request.data["workstation_policy"])
|
||||
|
||||
if related_type == "client":
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
|
||||
# Check and see if workstation policy changed and regenerate policies
|
||||
if (
|
||||
not client.workstation_policy
|
||||
or client.workstation_policy
|
||||
and client.workstation_policy.pk != policy.pk
|
||||
):
|
||||
client.workstation_policy = policy
|
||||
client.save()
|
||||
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if related_type == "site":
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
|
||||
# Check and see if workstation policy changed and regenerate policies
|
||||
if (
|
||||
not site.workstation_policy
|
||||
or site.workstation_policy
|
||||
and site.workstation_policy.pk != policy.pk
|
||||
):
|
||||
site.workstation_policy = policy
|
||||
site.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": site.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# server policy is set
|
||||
if "server_policy" in request.data and request.data["server_policy"] != 0:
|
||||
policy = get_object_or_404(Policy, pk=request.data["server_policy"])
|
||||
|
||||
if related_type == "client":
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
|
||||
# Check and see if server policy changed and regenerate policies
|
||||
if (
|
||||
not client.server_policy
|
||||
or client.server_policy
|
||||
and client.server_policy.pk != policy.pk
|
||||
):
|
||||
client.server_policy = policy
|
||||
client.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if related_type == "site":
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
|
||||
# Check and see if server policy changed and regenerate policies
|
||||
if (
|
||||
not site.server_policy
|
||||
or site.server_policy
|
||||
and site.server_policy.pk != policy.pk
|
||||
):
|
||||
site.server_policy = policy
|
||||
site.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": site.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# If workstation policy was cleared
|
||||
if (
|
||||
"workstation_policy" in request.data
|
||||
and request.data["workstation_policy"] == 0
|
||||
):
|
||||
if related_type == "client":
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
|
||||
# Check if workstation policy is set and update it to None
|
||||
if client.workstation_policy:
|
||||
|
||||
client.workstation_policy = None
|
||||
client.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if related_type == "site":
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
|
||||
# Check if workstation policy is set and update it to None
|
||||
if site.workstation_policy:
|
||||
|
||||
site.workstation_policy = None
|
||||
site.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": site.id},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# server policy cleared
|
||||
if "server_policy" in request.data and request.data["server_policy"] == 0:
|
||||
|
||||
if related_type == "client":
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
|
||||
# Check if server policy is set and update it to None
|
||||
if client.server_policy:
|
||||
|
||||
client.server_policy = None
|
||||
client.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": client.id},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if related_type == "site":
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
# Check if server policy is set and update it to None
|
||||
if site.server_policy:
|
||||
|
||||
site.server_policy = None
|
||||
site.save()
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": site.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# agent policies
|
||||
if related_type == "agent":
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
|
||||
if "policy" in request.data and request.data["policy"] != 0:
|
||||
policy = Policy.objects.get(pk=request.data["policy"])
|
||||
|
||||
# Check and see if policy changed and regenerate policies
|
||||
if not agent.policy or agent.policy and agent.policy.pk != policy.pk:
|
||||
agent.policy = policy
|
||||
agent.save()
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
else:
|
||||
if agent.policy:
|
||||
agent.policy = None
|
||||
agent.save()
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
# view to get policies set on client, site, and workstation
|
||||
def patch(self, request):
|
||||
related_type = request.data["type"]
|
||||
|
||||
# client, site, or agent pk
|
||||
pk = request.data["pk"]
|
||||
|
||||
if related_type == "agent":
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
return Response(RelatedAgentPolicySerializer(agent).data)
|
||||
|
||||
if related_type == "site":
|
||||
site = Site.objects.get(pk=pk)
|
||||
return Response(RelatedSitePolicySerializer(site).data)
|
||||
|
||||
if related_type == "client":
|
||||
client = Client.objects.get(pk=pk)
|
||||
return Response(RelatedClientPolicySerializer(client).data)
|
||||
|
||||
content = {"error": "Data was submitted incorrectly"}
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class UpdatePatchPolicy(APIView):
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class Command(BaseCommand):
|
||||
help = "Checks for orphaned tasks on all agents and removes them"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
agents = Agent.objects.only("pk", "last_seen", "overdue_time")
|
||||
agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time")
|
||||
online = [i for i in agents if i.status == "online"]
|
||||
for agent in online:
|
||||
remove_orphaned_win_tasks.delay(agent.pk)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-27 22:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0010_migrate_days_to_bitdays'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='alert_severity',
|
||||
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='None', max_length=30, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-28 04:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0011_automatedtask_alert_severity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='email_alert',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='email_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='text_alert',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='text_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-29 03:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0012_auto_20210128_0417'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='automatedtask',
|
||||
name='alert_severity',
|
||||
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-29 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0013_auto_20210129_0307'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='dashboard_alert',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-05 17:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0014_automatedtask_dashboard_alert'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='resolved_email_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='resolved_text_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-05 21:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0015_auto_20210205_1728'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('passing', 'Passing'), ('failing', 'Failing'), ('pending', 'Pending')], default='pending', max_length=30),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-10 15:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0016_automatedtask_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='automatedtask',
|
||||
name='email_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='automatedtask',
|
||||
name='resolved_email_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='automatedtask',
|
||||
name='resolved_text_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='automatedtask',
|
||||
name='text_sent',
|
||||
),
|
||||
]
|
||||
@@ -3,12 +3,20 @@ import random
|
||||
import string
|
||||
import datetime as dt
|
||||
|
||||
from django.utils import timezone as djangotime
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.fields import DateTimeField
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.utils import bitdays_to_string
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
RUN_TIME_DAY_CHOICES = [
|
||||
(0, "Monday"),
|
||||
(1, "Tuesday"),
|
||||
@@ -32,6 +40,12 @@ SYNC_STATUS_CHOICES = [
|
||||
("pendingdeletion", "Pending Deletion on Agent"),
|
||||
]
|
||||
|
||||
TASK_STATUS_CHOICES = [
|
||||
("passing", "Passing"),
|
||||
("failing", "Failing"),
|
||||
("pending", "Pending"),
|
||||
]
|
||||
|
||||
|
||||
class AutomatedTask(BaseAuditModel):
|
||||
agent = models.ForeignKey(
|
||||
@@ -93,9 +107,18 @@ class AutomatedTask(BaseAuditModel):
|
||||
execution_time = models.CharField(max_length=100, default="0.0000")
|
||||
last_run = models.DateTimeField(null=True, blank=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
status = models.CharField(
|
||||
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
|
||||
)
|
||||
sync_status = models.CharField(
|
||||
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
|
||||
)
|
||||
alert_severity = models.CharField(
|
||||
max_length=30, choices=SEVERITY_CHOICES, default="info"
|
||||
)
|
||||
email_alert = models.BooleanField(default=False)
|
||||
text_alert = models.BooleanField(default=False)
|
||||
dashboard_alert = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -140,22 +163,50 @@ class AutomatedTask(BaseAuditModel):
|
||||
def create_policy_task(self, agent=None, policy=None):
|
||||
from .tasks import create_win_task_schedule
|
||||
|
||||
# if policy is present, then this task is being copied to another policy
|
||||
# if agent is present, then this task is being created on an agent from a policy
|
||||
|
||||
# exit if neither are set or if both are set
|
||||
if not agent and not policy or agent and policy:
|
||||
return
|
||||
|
||||
assigned_check = None
|
||||
|
||||
# get correct assigned check to task if set
|
||||
if agent and self.assigned_check:
|
||||
assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.pk)
|
||||
# check if there is a matching check on the agent
|
||||
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
|
||||
assigned_check = agent.agentchecks.filter(
|
||||
parent_check=self.assigned_check.pk
|
||||
).first()
|
||||
# check was overriden by agent and we need to use that agents check
|
||||
else:
|
||||
if agent.agentchecks.filter(
|
||||
check_type=self.assigned_check.check_type, overriden_by_policy=True
|
||||
).exists():
|
||||
assigned_check = agent.agentchecks.filter(
|
||||
check_type=self.assigned_check.check_type,
|
||||
overriden_by_policy=True,
|
||||
).first()
|
||||
elif policy and self.assigned_check:
|
||||
assigned_check = policy.policychecks.get(name=self.assigned_check.name)
|
||||
if policy.policychecks.filter(name=self.assigned_check.name).exists():
|
||||
assigned_check = policy.policychecks.filter(
|
||||
name=self.assigned_check.name
|
||||
).first()
|
||||
else:
|
||||
assigned_check = policy.policychecks.filter(
|
||||
check_type=self.assigned_check.check_type
|
||||
).first()
|
||||
|
||||
task = AutomatedTask.objects.create(
|
||||
agent=agent,
|
||||
policy=policy,
|
||||
managed_by_policy=bool(agent),
|
||||
parent_task=(self.pk if agent else None),
|
||||
alert_severity=self.alert_severity,
|
||||
email_alert=self.email_alert,
|
||||
text_alert=self.text_alert,
|
||||
dashboard_alert=self.dashboard_alert,
|
||||
script=self.script,
|
||||
script_args=self.script_args,
|
||||
assigned_check=assigned_check,
|
||||
@@ -172,3 +223,215 @@ class AutomatedTask(BaseAuditModel):
|
||||
)
|
||||
|
||||
create_win_task_schedule.delay(task.pk)
|
||||
|
||||
def handle_alert(self) -> None:
|
||||
from alerts.models import Alert, AlertTemplate
|
||||
from autotasks.tasks import (
|
||||
handle_task_email_alert,
|
||||
handle_task_sms_alert,
|
||||
handle_resolved_task_sms_alert,
|
||||
handle_resolved_task_email_alert,
|
||||
)
|
||||
|
||||
self.status = "failing" if self.retcode != 0 else "passing"
|
||||
self.save()
|
||||
|
||||
# return if agent is in maintenance mode
|
||||
if self.agent.maintenance_mode:
|
||||
return
|
||||
|
||||
# see if agent has an alert template and use that
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
# resolve alert if it exists
|
||||
if self.status == "passing":
|
||||
if Alert.objects.filter(assigned_task=self, resolved=False).exists():
|
||||
alert = Alert.objects.get(assigned_task=self, resolved=False)
|
||||
alert.resolve()
|
||||
|
||||
# check if resolved email should be send
|
||||
if (
|
||||
not alert.resolved_email_sent
|
||||
and self.email_alert
|
||||
or alert_template
|
||||
and alert_template.task_email_on_resolved
|
||||
):
|
||||
handle_resolved_task_email_alert.delay(pk=alert.pk)
|
||||
|
||||
# check if resolved text should be sent
|
||||
if (
|
||||
not alert.resolved_sms_sent
|
||||
and self.text_alert
|
||||
or alert_template
|
||||
and alert_template.task_text_on_resolved
|
||||
):
|
||||
handle_resolved_task_sms_alert.delay(pk=alert.pk)
|
||||
|
||||
# check if resolved script should be run
|
||||
if (
|
||||
alert_template
|
||||
and alert_template.resolved_action
|
||||
and not alert.resolved_action_run
|
||||
):
|
||||
|
||||
r = self.agent.run_script(
|
||||
scriptpk=alert_template.resolved_action.pk,
|
||||
args=alert_template.resolved_action_args,
|
||||
timeout=alert_template.resolved_action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.resolved_action_retcode = r["retcode"]
|
||||
alert.resolved_action_stdout = r["stdout"]
|
||||
alert.resolved_action_stderr = r["stderr"]
|
||||
alert.resolved_action_execution_time = "{:.4f}".format(
|
||||
r["execution_time"]
|
||||
)
|
||||
alert.resolved_action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
|
||||
)
|
||||
|
||||
# create alert if task is failing
|
||||
else:
|
||||
if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
|
||||
alert = Alert.create_task_alert(self)
|
||||
else:
|
||||
alert = Alert.objects.get(assigned_task=self, resolved=False)
|
||||
|
||||
# check if alert severity changed on task and update the alert
|
||||
if self.alert_severity != alert.severity:
|
||||
alert.severity = self.alert_severity
|
||||
alert.save(update_fields=["severity"])
|
||||
|
||||
# create alert in dashboard if enabled
|
||||
if (
|
||||
self.dashboard_alert
|
||||
or alert_template
|
||||
and alert_template.task_always_alert
|
||||
):
|
||||
alert.hidden = False
|
||||
alert.save()
|
||||
|
||||
# send email if enabled
|
||||
if (
|
||||
not alert.email_sent
|
||||
and self.email_alert
|
||||
or alert_template
|
||||
and self.alert_severity in alert_template.task_email_alert_severity
|
||||
and alert_template.check_always_email
|
||||
):
|
||||
handle_task_email_alert.delay(
|
||||
pk=alert.pk,
|
||||
alert_template=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# send text if enabled
|
||||
if (
|
||||
not alert.sms_sent
|
||||
and self.text_alert
|
||||
or alert_template
|
||||
and self.alert_severity in alert_template.task_text_alert_severity
|
||||
and alert_template.check_always_text
|
||||
):
|
||||
handle_task_sms_alert.delay(
|
||||
pk=alert.pk,
|
||||
alert_template=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# check if any scripts should be run
|
||||
if alert_template and alert_template.action and not alert.action_run:
|
||||
r = self.agent.run_script(
|
||||
scriptpk=alert_template.action.pk,
|
||||
args=alert_template.action_args,
|
||||
timeout=alert_template.action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.action_retcode = r["retcode"]
|
||||
alert.action_stdout = r["stdout"]
|
||||
alert.action_stderr = r["stderr"]
|
||||
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
|
||||
alert.action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
|
||||
)
|
||||
|
||||
def send_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_mail(subject, body, alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
|
||||
def send_resolved_email(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
|
||||
def send_resolved_sms(self):
|
||||
from core.models import CoreSettings
|
||||
|
||||
alert_template = self.agent.get_alert_template()
|
||||
CORE = CoreSettings.objects.first()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = (
|
||||
subject
|
||||
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
|
||||
)
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
|
||||
@@ -6,6 +6,9 @@ from django.conf import settings
|
||||
import pytz
|
||||
from django.utils import timezone as djangotime
|
||||
from packaging import version as pyver
|
||||
from typing import Union
|
||||
import random
|
||||
from time import sleep
|
||||
|
||||
from .models import AutomatedTask
|
||||
from logs.models import PendingAction
|
||||
@@ -243,3 +246,85 @@ def remove_orphaned_win_tasks(agentpk):
|
||||
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
|
||||
|
||||
logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending email
|
||||
if not alert.email_sent:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.assigned_task.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send an email only if the last email sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.email_sent < delta:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.assigned_task.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending text
|
||||
if not alert.sms_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
alert.assigned_task.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send a text only if the last text sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.sms_sent < delta:
|
||||
sleep(random.randint(1, 3))
|
||||
alert.assigned_task.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_resolved_task_sms_alert(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending text
|
||||
if not alert.resolved_sms_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
alert.assigned_task.send_resolved_sms()
|
||||
alert.resolved_sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_resolved_task_email_alert(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending email
|
||||
if not alert.resolved_email_sent:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.assigned_task.send_resolved_email()
|
||||
alert.resolved_email_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
@@ -150,7 +150,9 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
update_policy_task_fields_task.assert_called_with(policy_task.id, True)
|
||||
update_policy_task_fields_task.assert_called_with(
|
||||
policy_task.id, update_agent=True
|
||||
)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
|
||||
@@ -81,6 +81,20 @@ class AutoTask(APIView):
|
||||
}
|
||||
return Response(AutoTaskSerializer(agent, context=ctx).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
from automation.tasks import update_policy_task_fields_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
if task.policy:
|
||||
update_policy_task_fields_task.delay(task.pk)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request, pk):
|
||||
from automation.tasks import update_policy_task_fields_task
|
||||
|
||||
@@ -93,7 +107,7 @@ class AutoTask(APIView):
|
||||
enable_or_disable_win_task.delay(pk=task.pk, action=action)
|
||||
|
||||
else:
|
||||
update_policy_task_fields_task.delay(task.pk, action)
|
||||
update_policy_task_fields_task.delay(task.pk, update_agent=True)
|
||||
|
||||
task.enabled = action
|
||||
task.save(update_fields=["enabled"])
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
from .models import Check
|
||||
from model_bakery.recipe import Recipe, seq
|
||||
from model_bakery.recipe import Recipe
|
||||
|
||||
check = Recipe(Check)
|
||||
check = Recipe("checks.Check")
|
||||
|
||||
diskspace_check = check.extend(check_type="diskspace", disk="C:", threshold=75)
|
||||
diskspace_check = check.extend(
|
||||
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
|
||||
)
|
||||
|
||||
cpuload_check = check.extend(check_type="cpuload", threshold=75)
|
||||
cpuload_check = check.extend(
|
||||
check_type="cpuload", warning_threshold=30, error_threshold=75
|
||||
)
|
||||
|
||||
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
|
||||
|
||||
memory_check = check.extend(check_type="memory", threshold=75)
|
||||
memory_check = check.extend(
|
||||
check_type="memory", warning_threshold=30, error_threshold=75
|
||||
)
|
||||
|
||||
winsvc_check = check.extend(
|
||||
check_type="winsvc",
|
||||
|
||||
43
api/tacticalrmm/checks/migrations/0016_auto_20210123_0149.py
Normal file
43
api/tacticalrmm/checks/migrations/0016_auto_20210123_0149.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-23 01:49
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0015_auto_20210110_1808'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='check',
|
||||
name='threshold',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='alert_severity',
|
||||
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='error_threshold',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='info_return_codes',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='warning_return_codes',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='warning_threshold',
|
||||
field=models.PositiveIntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-29 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0016_auto_20210123_0149'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='dashboard_alert',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/checks/migrations/0018_auto_20210205_1647.py
Normal file
18
api/tacticalrmm/checks/migrations/0018_auto_20210205_1647.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-05 16:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0017_check_dashboard_alert'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='check',
|
||||
name='alert_severity',
|
||||
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15, null=True),
|
||||
),
|
||||
]
|
||||
23
api/tacticalrmm/checks/migrations/0019_auto_20210205_1728.py
Normal file
23
api/tacticalrmm/checks/migrations/0019_auto_20210205_1728.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-05 17:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0018_auto_20210205_1647'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='resolved_email_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='check',
|
||||
name='resolved_text_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
29
api/tacticalrmm/checks/migrations/0020_auto_20210210_1512.py
Normal file
29
api/tacticalrmm/checks/migrations/0020_auto_20210210_1512.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-10 15:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0019_auto_20210205_1728'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='check',
|
||||
name='email_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='check',
|
||||
name='resolved_email_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='check',
|
||||
name='resolved_text_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='check',
|
||||
name='text_sent',
|
||||
),
|
||||
]
|
||||
24
api/tacticalrmm/checks/migrations/0021_auto_20210212_1429.py
Normal file
24
api/tacticalrmm/checks/migrations/0021_auto_20210212_1429.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 14:29
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0020_auto_20210210_1512'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='check',
|
||||
name='error_threshold',
|
||||
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='check',
|
||||
name='warning_threshold',
|
||||
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
|
||||
),
|
||||
]
|
||||
@@ -3,18 +3,31 @@ import string
|
||||
import os
|
||||
import json
|
||||
import pytz
|
||||
from statistics import mean, mode
|
||||
from statistics import mean
|
||||
|
||||
from django.utils import timezone as djangotime
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from rest_framework.fields import JSONField
|
||||
from typing import List, Any
|
||||
from typing import Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.models import CoreSettings
|
||||
from logs.models import BaseAuditModel
|
||||
from .tasks import handle_check_email_alert_task, handle_check_sms_alert_task
|
||||
from .tasks import (
|
||||
handle_check_email_alert_task,
|
||||
handle_check_sms_alert_task,
|
||||
handle_resolved_check_email_alert_task,
|
||||
handle_resolved_check_sms_alert_task,
|
||||
)
|
||||
from .utils import bytes2human
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
CHECK_TYPE_CHOICES = [
|
||||
("diskspace", "Disk Space Check"),
|
||||
@@ -84,18 +97,34 @@ class Check(BaseAuditModel):
|
||||
last_run = models.DateTimeField(null=True, blank=True)
|
||||
email_alert = models.BooleanField(default=False)
|
||||
text_alert = models.BooleanField(default=False)
|
||||
dashboard_alert = models.BooleanField(default=False)
|
||||
fails_b4_alert = models.PositiveIntegerField(default=1)
|
||||
fail_count = models.PositiveIntegerField(default=0)
|
||||
email_sent = models.DateTimeField(null=True, blank=True)
|
||||
text_sent = models.DateTimeField(null=True, blank=True)
|
||||
outage_history = models.JSONField(null=True, blank=True) # store
|
||||
extra_details = models.JSONField(null=True, blank=True)
|
||||
|
||||
# check specific fields
|
||||
|
||||
# for eventlog, script, ip, and service alert severity
|
||||
alert_severity = models.CharField(
|
||||
max_length=15,
|
||||
choices=SEVERITY_CHOICES,
|
||||
default="warning",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# threshold percent for diskspace, cpuload or memory check
|
||||
threshold = models.PositiveIntegerField(
|
||||
null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(99)]
|
||||
error_threshold = models.PositiveIntegerField(
|
||||
validators=[MinValueValidator(0), MaxValueValidator(99)],
|
||||
null=True,
|
||||
blank=True,
|
||||
default=0,
|
||||
)
|
||||
warning_threshold = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(99)],
|
||||
default=0,
|
||||
)
|
||||
# diskcheck i.e C:, D: etc
|
||||
disk = models.CharField(max_length=2, null=True, blank=True)
|
||||
@@ -115,6 +144,18 @@ class Check(BaseAuditModel):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
info_return_codes = ArrayField(
|
||||
models.PositiveIntegerField(),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
warning_return_codes = ArrayField(
|
||||
models.PositiveIntegerField(),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
timeout = models.PositiveIntegerField(null=True, blank=True)
|
||||
stdout = models.TextField(null=True, blank=True)
|
||||
stderr = models.TextField(null=True, blank=True)
|
||||
@@ -159,11 +200,25 @@ class Check(BaseAuditModel):
|
||||
@property
|
||||
def readable_desc(self):
|
||||
if self.check_type == "diskspace":
|
||||
return f"{self.get_check_type_display()}: Drive {self.disk} < {self.threshold}%"
|
||||
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
|
||||
elif self.check_type == "ping":
|
||||
return f"{self.get_check_type_display()}: {self.name}"
|
||||
elif self.check_type == "cpuload" or self.check_type == "memory":
|
||||
return f"{self.get_check_type_display()} > {self.threshold}%"
|
||||
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
return f"{self.get_check_type_display()} > {text}"
|
||||
elif self.check_type == "winsvc":
|
||||
return f"{self.get_check_type_display()}: {self.svc_display_name}"
|
||||
elif self.check_type == "eventlog":
|
||||
@@ -188,15 +243,13 @@ class Check(BaseAuditModel):
|
||||
return self.last_run
|
||||
|
||||
@property
|
||||
def non_editable_fields(self):
|
||||
def non_editable_fields(self) -> List[str]:
|
||||
return [
|
||||
"check_type",
|
||||
"status",
|
||||
"more_info",
|
||||
"last_run",
|
||||
"fail_count",
|
||||
"email_sent",
|
||||
"text_sent",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"stdout",
|
||||
@@ -215,10 +268,148 @@ class Check(BaseAuditModel):
|
||||
"modified_time",
|
||||
]
|
||||
|
||||
def add_check_history(self, value, more_info=None):
|
||||
def handle_alert(self) -> None:
|
||||
from alerts.models import Alert, AlertTemplate
|
||||
|
||||
# return if agent is in maintenance mode
|
||||
if self.agent.maintenance_mode:
|
||||
return
|
||||
|
||||
# see if agent has an alert template and use that
|
||||
alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
|
||||
|
||||
# resolve alert if it exists
|
||||
if self.status == "passing":
|
||||
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
|
||||
alert = Alert.objects.get(assigned_check=self, resolved=False)
|
||||
alert.resolve()
|
||||
|
||||
# check if a resolved email notification should be send
|
||||
if (
|
||||
alert_template
|
||||
and alert_template.check_email_on_resolved
|
||||
and not alert.resolved_email_sent
|
||||
):
|
||||
handle_resolved_check_email_alert_task.delay(pk=alert.pk)
|
||||
|
||||
# check if resolved text should be sent
|
||||
if (
|
||||
alert_template
|
||||
and alert_template.check_text_on_resolved
|
||||
and not alert.resolved_sms_sent
|
||||
):
|
||||
handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
|
||||
|
||||
# check if resolved script should be run
|
||||
if (
|
||||
alert_template
|
||||
and alert_template.resolved_action
|
||||
and not alert.resolved_action_run
|
||||
):
|
||||
r = self.agent.run_script(
|
||||
scriptpk=alert_template.resolved_action.pk,
|
||||
args=alert_template.resolved_action_args,
|
||||
timeout=alert_template.resolved_action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.resolved_action_retcode = r["retcode"]
|
||||
alert.resolved_action_stdout = r["stdout"]
|
||||
alert.resolved_action_stderr = r["stderr"]
|
||||
alert.resolved_action_execution_time = "{:.4f}".format(
|
||||
r["execution_time"]
|
||||
)
|
||||
alert.resolved_action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
|
||||
)
|
||||
|
||||
elif self.fail_count >= self.fails_b4_alert:
|
||||
if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
|
||||
alert = Alert.create_check_alert(self)
|
||||
else:
|
||||
alert = Alert.objects.get(assigned_check=self, resolved=False)
|
||||
|
||||
# check if alert severity changed on check and update the alert
|
||||
if self.alert_severity != alert.severity:
|
||||
alert.severity = self.alert_severity
|
||||
alert.save(update_fields=["severity"])
|
||||
|
||||
# create alert in dashboard if enabled
|
||||
if (
|
||||
self.dashboard_alert
|
||||
or alert_template
|
||||
and self.alert_severity in alert_template.check_dashboard_alert_severity
|
||||
and alert_template.check_always_alert
|
||||
):
|
||||
alert.hidden = False
|
||||
alert.save()
|
||||
|
||||
# send email if enabled
|
||||
if (
|
||||
not alert.email_sent
|
||||
and self.email_alert
|
||||
or alert_template
|
||||
and self.alert_severity in alert_template.check_email_alert_severity
|
||||
and alert_template.check_always_email
|
||||
):
|
||||
handle_check_email_alert_task.delay(
|
||||
pk=alert.pk,
|
||||
alert_interval=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# send text if enabled
|
||||
if (
|
||||
not alert.sms_sent
|
||||
and self.text_alert
|
||||
or alert_template
|
||||
and self.alert_severity in alert_template.check_text_alert_severity
|
||||
and alert_template.check_always_text
|
||||
):
|
||||
handle_check_sms_alert_task.delay(
|
||||
pk=alert.pk,
|
||||
alert_interval=alert_template.check_periodic_alert_days
|
||||
if alert_template
|
||||
else None,
|
||||
)
|
||||
|
||||
# check if any scripts should be run
|
||||
if alert_template and alert_template.action and not alert.action_run:
|
||||
r = self.agent.run_script(
|
||||
scriptpk=alert_template.action.pk,
|
||||
args=alert_template.action_args,
|
||||
timeout=alert_template.action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
run_on_any=True,
|
||||
)
|
||||
|
||||
# command was successful
|
||||
if type(r) == dict:
|
||||
alert.action_retcode = r["retcode"]
|
||||
alert.action_stdout = r["stdout"]
|
||||
alert.action_stderr = r["stderr"]
|
||||
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
|
||||
alert.action_run = djangotime.now()
|
||||
alert.save()
|
||||
else:
|
||||
logger.error(
|
||||
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
|
||||
)
|
||||
|
||||
def add_check_history(self, value: int, more_info: Any = None) -> None:
|
||||
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
|
||||
|
||||
def handle_checkv2(self, data):
|
||||
|
||||
# cpuload or mem checks
|
||||
if self.check_type == "cpuload" or self.check_type == "memory":
|
||||
|
||||
@@ -231,8 +422,12 @@ class Check(BaseAuditModel):
|
||||
|
||||
avg = int(mean(self.history))
|
||||
|
||||
if avg > self.threshold:
|
||||
if self.error_threshold and avg > self.error_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
elif self.warning_threshold and avg > self.warning_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "warning"
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
@@ -246,17 +441,26 @@ class Check(BaseAuditModel):
|
||||
total = bytes2human(data["total"])
|
||||
free = bytes2human(data["free"])
|
||||
|
||||
if (100 - percent_used) < self.threshold:
|
||||
if self.error_threshold and (100 - percent_used) < self.error_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
elif (
|
||||
self.warning_threshold
|
||||
and (100 - percent_used) < self.warning_threshold
|
||||
):
|
||||
self.status = "failing"
|
||||
self.alert_severity = "warning"
|
||||
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
self.more_info = f"Total: {total}B, Free: {free}B"
|
||||
|
||||
# add check history
|
||||
self.add_check_history(percent_used)
|
||||
self.add_check_history(100 - percent_used)
|
||||
else:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
self.more_info = f"Disk {self.disk} does not exist"
|
||||
|
||||
self.save(update_fields=["more_info"])
|
||||
@@ -273,8 +477,15 @@ class Check(BaseAuditModel):
|
||||
# golang agent
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
|
||||
if data["retcode"] != 0:
|
||||
if data["retcode"] in self.info_return_codes:
|
||||
self.alert_severity = "info"
|
||||
self.status = "failing"
|
||||
elif data["retcode"] in self.warning_return_codes:
|
||||
self.alert_severity = "warning"
|
||||
self.status = "failing"
|
||||
elif data["retcode"] != 0:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
@@ -428,59 +639,16 @@ class Check(BaseAuditModel):
|
||||
# handle status
|
||||
if self.status == "failing":
|
||||
self.fail_count += 1
|
||||
self.save(update_fields=["status", "fail_count"])
|
||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||
|
||||
elif self.status == "passing":
|
||||
if self.fail_count != 0:
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["status", "fail_count"])
|
||||
else:
|
||||
self.save(update_fields=["status"])
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||
|
||||
if self.fail_count >= self.fails_b4_alert:
|
||||
if self.email_alert:
|
||||
handle_check_email_alert_task.delay(self.pk)
|
||||
if self.text_alert:
|
||||
handle_check_sms_alert_task.delay(self.pk)
|
||||
self.handle_alert()
|
||||
|
||||
return self.status
|
||||
|
||||
def handle_check(self, data):
|
||||
if self.check_type != "cpuload" and self.check_type != "memory":
|
||||
|
||||
if data["status"] == "passing" and self.fail_count != 0:
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["fail_count"])
|
||||
|
||||
elif data["status"] == "failing":
|
||||
self.fail_count += 1
|
||||
self.save(update_fields=["fail_count"])
|
||||
|
||||
else:
|
||||
self.history.append(data["percent"])
|
||||
|
||||
if len(self.history) > 15:
|
||||
self.history = self.history[-15:]
|
||||
|
||||
self.save(update_fields=["history"])
|
||||
|
||||
avg = int(mean(self.history))
|
||||
|
||||
if avg > self.threshold:
|
||||
self.status = "failing"
|
||||
self.fail_count += 1
|
||||
self.save(update_fields=["status", "fail_count"])
|
||||
else:
|
||||
self.status = "passing"
|
||||
if self.fail_count != 0:
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["status", "fail_count"])
|
||||
else:
|
||||
self.save(update_fields=["status"])
|
||||
|
||||
if self.email_alert and self.fail_count >= self.fails_b4_alert:
|
||||
handle_check_email_alert_task.delay(self.pk)
|
||||
|
||||
@staticmethod
|
||||
def serialize(check):
|
||||
# serializes the check and returns json
|
||||
@@ -514,17 +682,22 @@ class Check(BaseAuditModel):
|
||||
managed_by_policy=bool(agent),
|
||||
parent_check=(self.pk if agent else None),
|
||||
name=self.name,
|
||||
alert_severity=self.alert_severity,
|
||||
check_type=self.check_type,
|
||||
email_alert=self.email_alert,
|
||||
dashboard_alert=self.dashboard_alert,
|
||||
text_alert=self.text_alert,
|
||||
fails_b4_alert=self.fails_b4_alert,
|
||||
extra_details=self.extra_details,
|
||||
threshold=self.threshold,
|
||||
error_threshold=self.error_threshold,
|
||||
warning_threshold=self.warning_threshold,
|
||||
disk=self.disk,
|
||||
ip=self.ip,
|
||||
script=self.script,
|
||||
script_args=self.script_args,
|
||||
timeout=self.timeout,
|
||||
info_return_codes=self.info_return_codes,
|
||||
warning_return_codes=self.warning_return_codes,
|
||||
svc_name=self.svc_name,
|
||||
svc_display_name=self.svc_display_name,
|
||||
pass_if_start_pending=self.pass_if_start_pending,
|
||||
@@ -566,19 +739,27 @@ class Check(BaseAuditModel):
|
||||
def send_email(self):
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
|
||||
body: str = ""
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
if self.check_type == "diskspace":
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
|
||||
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%"
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
|
||||
elif self.check_type == "script":
|
||||
|
||||
@@ -592,26 +773,29 @@ class Check(BaseAuditModel):
|
||||
body = self.more_info
|
||||
|
||||
elif self.check_type == "cpuload" or self.check_type == "memory":
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
avg = int(mean(self.history))
|
||||
|
||||
if self.check_type == "cpuload":
|
||||
body = (
|
||||
subject
|
||||
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
|
||||
)
|
||||
body = subject + f" - Average CPU utilization: {avg}%, {text}"
|
||||
|
||||
elif self.check_type == "memory":
|
||||
body = (
|
||||
subject
|
||||
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
|
||||
)
|
||||
body = subject + f" - Average memory usage: {avg}%, {text}"
|
||||
|
||||
elif self.check_type == "winsvc":
|
||||
|
||||
status = list(
|
||||
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
|
||||
)[0]["status"]
|
||||
try:
|
||||
status = list(
|
||||
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
|
||||
)[0]["status"]
|
||||
# catch services that don't exist if policy check
|
||||
except:
|
||||
status = "Unknown"
|
||||
|
||||
body = subject + f" - Status: {status.upper()}"
|
||||
|
||||
@@ -637,11 +821,13 @@ class Check(BaseAuditModel):
|
||||
except:
|
||||
continue
|
||||
|
||||
CORE.send_mail(subject, body)
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
body: str = ""
|
||||
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
@@ -649,27 +835,33 @@ class Check(BaseAuditModel):
|
||||
subject = f"{self} Failed"
|
||||
|
||||
if self.check_type == "diskspace":
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%"
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
elif self.check_type == "script":
|
||||
body = subject + f" - Return code: {self.retcode}"
|
||||
elif self.check_type == "ping":
|
||||
body = subject
|
||||
elif self.check_type == "cpuload" or self.check_type == "memory":
|
||||
text = ""
|
||||
if self.warning_threshold:
|
||||
text += f" Warning Threshold: {self.warning_threshold}%"
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
avg = int(mean(self.history))
|
||||
if self.check_type == "cpuload":
|
||||
body = (
|
||||
subject
|
||||
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
|
||||
)
|
||||
body = subject + f" - Average CPU utilization: {avg}%, {text}"
|
||||
elif self.check_type == "memory":
|
||||
body = (
|
||||
subject
|
||||
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
|
||||
)
|
||||
body = subject + f" - Average memory usage: {avg}%, {text}"
|
||||
elif self.check_type == "winsvc":
|
||||
status = list(
|
||||
filter(lambda x: x["name"] == self.svc_name, self.agent.services)
|
||||
@@ -678,7 +870,21 @@ class Check(BaseAuditModel):
|
||||
elif self.check_type == "eventlog":
|
||||
body = subject
|
||||
|
||||
CORE.send_sms(body)
|
||||
CORE.send_sms(body, alert_template=alert_template)
|
||||
|
||||
def send_resolved_email(self):
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
body = f"{self} is now back to normal"
|
||||
|
||||
CORE.send_mail(subject, body, alert_template=alert_template)
|
||||
|
||||
def send_resolved_sms(self):
|
||||
CORE = CoreSettings.objects.first()
|
||||
alert_template = self.agent.get_alert_template()
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
|
||||
CORE.send_sms(subject, alert_template=alert_template)
|
||||
|
||||
|
||||
class CheckHistory(models.Model):
|
||||
|
||||
@@ -40,19 +40,35 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
check_type = val["check_type"]
|
||||
except KeyError:
|
||||
return val
|
||||
|
||||
# disk checks
|
||||
# make sure no duplicate diskchecks exist for an agent/policy
|
||||
if check_type == "diskspace" and not self.instance: # only on create
|
||||
checks = (
|
||||
Check.objects.filter(**self.context)
|
||||
.filter(check_type="diskspace")
|
||||
.exclude(managed_by_policy=True)
|
||||
)
|
||||
for check in checks:
|
||||
if val["disk"] in check.disk:
|
||||
raise serializers.ValidationError(
|
||||
f"A disk check for Drive {val['disk']} already exists!"
|
||||
)
|
||||
if check_type == "diskspace":
|
||||
if not self.instance: # only on create
|
||||
checks = (
|
||||
Check.objects.filter(**self.context)
|
||||
.filter(check_type="diskspace")
|
||||
.exclude(managed_by_policy=True)
|
||||
)
|
||||
for check in checks:
|
||||
if val["disk"] in check.disk:
|
||||
raise serializers.ValidationError(
|
||||
f"A disk check for Drive {val['disk']} already exists!"
|
||||
)
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
val["warning_threshold"] < val["error_threshold"]
|
||||
and val["warning_threshold"] > 0
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be greater than Error Threshold"
|
||||
)
|
||||
|
||||
# ping checks
|
||||
if check_type == "ping":
|
||||
@@ -75,6 +91,20 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
"A cpuload check for this agent already exists"
|
||||
)
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
val["warning_threshold"] > val["error_threshold"]
|
||||
and val["warning_threshold"] > 0
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be less than Error Threshold"
|
||||
)
|
||||
|
||||
if check_type == "memory" and not self.instance:
|
||||
if (
|
||||
Check.objects.filter(**self.context, check_type="memory")
|
||||
@@ -85,6 +115,20 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
"A memory check for this agent already exists"
|
||||
)
|
||||
|
||||
if not val["warning_threshold"] and not val["error_threshold"]:
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold or Error Threshold must be set"
|
||||
)
|
||||
|
||||
if (
|
||||
val["warning_threshold"] > val["error_threshold"]
|
||||
and val["warning_threshold"] > 0
|
||||
and val["error_threshold"] > 0
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
f"Warning threshold must be less than Error Threshold"
|
||||
)
|
||||
|
||||
return val
|
||||
|
||||
|
||||
@@ -95,101 +139,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
# for the windows agent
|
||||
# only send data needed for agent to run a check
|
||||
|
||||
assigned_task = serializers.SerializerMethodField()
|
||||
script = ScriptSerializer(read_only=True)
|
||||
|
||||
def get_assigned_task(self, obj):
|
||||
if obj.assignedtask.exists():
|
||||
# this will not break agents on version 0.10.2 or lower
|
||||
# newer agents once released will properly handle multiple tasks assigned to a check
|
||||
task = obj.assignedtask.first()
|
||||
return AssignedTaskCheckRunnerField(task).data
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
exclude = [
|
||||
"policy",
|
||||
"managed_by_policy",
|
||||
"overriden_by_policy",
|
||||
"parent_check",
|
||||
"name",
|
||||
"more_info",
|
||||
"last_run",
|
||||
"email_alert",
|
||||
"text_alert",
|
||||
"fails_b4_alert",
|
||||
"fail_count",
|
||||
"email_sent",
|
||||
"text_sent",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"retcode",
|
||||
"execution_time",
|
||||
"svc_display_name",
|
||||
"svc_policy_mode",
|
||||
"created_by",
|
||||
"created_time",
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"history",
|
||||
]
|
||||
|
||||
|
||||
class CheckRunnerGetSerializerV2(serializers.ModelSerializer):
|
||||
# for the windows __python__ agent
|
||||
# only send data needed for agent to run a check
|
||||
|
||||
assigned_tasks = serializers.SerializerMethodField()
|
||||
script = ScriptSerializer(read_only=True)
|
||||
|
||||
def get_assigned_tasks(self, obj):
|
||||
if obj.assignedtask.exists():
|
||||
tasks = obj.assignedtask.all()
|
||||
return AssignedTaskCheckRunnerField(tasks, many=True).data
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
exclude = [
|
||||
"policy",
|
||||
"managed_by_policy",
|
||||
"overriden_by_policy",
|
||||
"parent_check",
|
||||
"name",
|
||||
"more_info",
|
||||
"last_run",
|
||||
"email_alert",
|
||||
"text_alert",
|
||||
"fails_b4_alert",
|
||||
"fail_count",
|
||||
"email_sent",
|
||||
"text_sent",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"retcode",
|
||||
"execution_time",
|
||||
"svc_display_name",
|
||||
"svc_policy_mode",
|
||||
"created_by",
|
||||
"created_time",
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"history",
|
||||
]
|
||||
|
||||
|
||||
class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
|
||||
# for the windows __golang__ agent
|
||||
# only send data needed for agent to run a check
|
||||
# the difference here is in the script serializer
|
||||
# script checks no longer rely on salt and are executed directly by the go agent
|
||||
|
||||
assigned_tasks = serializers.SerializerMethodField()
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
|
||||
@@ -212,8 +162,6 @@ class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
|
||||
"text_alert",
|
||||
"fails_b4_alert",
|
||||
"fail_count",
|
||||
"email_sent",
|
||||
"text_sent",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"stdout",
|
||||
|
||||
@@ -1,57 +1,90 @@
|
||||
import datetime as dt
|
||||
import random
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_check_email_alert_task(pk):
|
||||
from .models import Check
|
||||
def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
check = Check.objects.get(pk=pk)
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
if not check.agent.maintenance_mode:
|
||||
# first time sending email
|
||||
if not check.email_sent:
|
||||
sleep(random.randint(1, 10))
|
||||
check.send_email()
|
||||
check.email_sent = djangotime.now()
|
||||
check.save(update_fields=["email_sent"])
|
||||
else:
|
||||
# send an email only if the last email sent is older than 24 hours
|
||||
delta = djangotime.now() - dt.timedelta(hours=24)
|
||||
if check.email_sent < delta:
|
||||
# first time sending email
|
||||
if not alert.email_sent:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.assigned_check.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send an email only if the last email sent is older than alert interval
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.email_sent < delta:
|
||||
sleep(random.randint(1, 10))
|
||||
check.send_email()
|
||||
check.email_sent = djangotime.now()
|
||||
check.save(update_fields=["email_sent"])
|
||||
alert.assigned_check.send_email()
|
||||
alert.email_sent = djangotime.now()
|
||||
alert.save(update_fields=["email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_check_sms_alert_task(pk):
|
||||
from .models import Check
|
||||
def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
check = Check.objects.get(pk=pk)
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
if not check.agent.maintenance_mode:
|
||||
# first time sending text
|
||||
if not check.text_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
check.send_sms()
|
||||
check.text_sent = djangotime.now()
|
||||
check.save(update_fields=["text_sent"])
|
||||
else:
|
||||
# first time sending text
|
||||
if not alert.sms_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
alert.assigned_check.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
else:
|
||||
if alert_interval:
|
||||
# send a text only if the last text sent is older than 24 hours
|
||||
delta = djangotime.now() - dt.timedelta(hours=24)
|
||||
if check.text_sent < delta:
|
||||
delta = djangotime.now() - dt.timedelta(days=alert_interval)
|
||||
if alert.sms_sent < delta:
|
||||
sleep(random.randint(1, 3))
|
||||
check.send_sms()
|
||||
check.text_sent = djangotime.now()
|
||||
check.save(update_fields=["text_sent"])
|
||||
alert.assigned_check.send_sms()
|
||||
alert.sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_resolved_check_sms_alert_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending text
|
||||
if not alert.resolved_sms_sent:
|
||||
sleep(random.randint(1, 3))
|
||||
alert.assigned_check.send_resolved_sms()
|
||||
alert.resolved_sms_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_sms_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_resolved_check_email_alert_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
alert = Alert.objects.get(pk=pk)
|
||||
|
||||
# first time sending email
|
||||
if not alert.resolved_email_sent:
|
||||
sleep(random.randint(1, 10))
|
||||
alert.assigned_check.send_resolved_email()
|
||||
alert.resolved_email_sent = djangotime.now()
|
||||
alert.save(update_fields=["resolved_email_sent"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ from checks.models import CheckHistory
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from .serializers import CheckSerializer
|
||||
from django.utils import timezone as djangotime
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker
|
||||
from itertools import cycle
|
||||
|
||||
|
||||
class TestCheckViews(TacticalTestCase):
|
||||
@@ -23,7 +23,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
self.check_not_authenticated("post", url)
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_disk_check(self):
|
||||
# setup data
|
||||
@@ -36,7 +36,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"threshold": 55,
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
@@ -50,7 +51,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"threshold": 55,
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
@@ -58,6 +60,38 @@ class TestCheckViews(TacticalTestCase):
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because both error and warning threshold are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because both error is greater than warning threshold
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 50,
|
||||
"warning_threshold": 30,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_cpuload_check(self):
|
||||
url = "/checks/checks/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -65,7 +99,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"threshold": 66,
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
@@ -73,7 +108,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
payload["threshold"] = 87
|
||||
payload["error_threshold"] = 87
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(
|
||||
@@ -81,6 +116,36 @@ class TestCheckViews(TacticalTestCase):
|
||||
"A cpuload check for this agent already exists",
|
||||
)
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# should fail because error is less than warning
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 10,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_memory_check(self):
|
||||
url = "/checks/checks/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -88,7 +153,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"threshold": 78,
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
},
|
||||
}
|
||||
@@ -96,7 +162,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
payload["threshold"] = 55
|
||||
payload["error_threshold"] = 55
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(
|
||||
@@ -104,6 +170,34 @@ class TestCheckViews(TacticalTestCase):
|
||||
"A memory check for this agent already exists",
|
||||
)
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# should fail because error is less than warning
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"error_threshold": 10,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_get_policy_disk_check(self):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
@@ -129,11 +223,37 @@ class TestCheckViews(TacticalTestCase):
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "M:",
|
||||
"threshold": 86,
|
||||
"error_threshold": 86,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 2,
|
||||
},
|
||||
}
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"policy": policy.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# should fail because warning is less than error
|
||||
invalid_payload = {
|
||||
"policy": policy.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"error_threshold": 80,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, valid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -143,7 +263,8 @@ class TestCheckViews(TacticalTestCase):
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "M:",
|
||||
"threshold": 34,
|
||||
"error_threshold": 34,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
@@ -184,6 +305,48 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("patch", url_a)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_checks(self, nats_cmd):
|
||||
agent = baker.make_recipe("agents.agent", version="1.4.1")
|
||||
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
|
||||
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
|
||||
|
||||
url = f"/checks/runchecks/{agent_old.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
|
||||
|
||||
url = f"/checks/runchecks/{agent_b4_141.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "busy"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}")
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "ok"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}")
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "timeout"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
self.assertEqual(r.json(), "Unable to contact the agent")
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_check_history(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from packaging import version as pyver
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
@@ -168,8 +169,17 @@ def run_checks(request, pk):
|
||||
if not agent.has_nats:
|
||||
return notify_error("Requires agent version 1.1.0 or greater")
|
||||
|
||||
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
|
||||
return Response(agent.hostname)
|
||||
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
|
||||
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
|
||||
if r == "busy":
|
||||
return notify_error(f"Checks are already running on {agent.hostname}")
|
||||
elif r == "ok":
|
||||
return Response(f"Checks will now be re-run on {agent.hostname}")
|
||||
else:
|
||||
return notify_error("Unable to contact the agent")
|
||||
else:
|
||||
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
|
||||
return Response(f"Checks will now be re-run on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 14:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0004_auto_20210212_1408'),
|
||||
('clients', '0008_auto_20201103_1430'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='alert_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='alerts.alerttemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='alert_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='alerts.alerttemplate'),
|
||||
),
|
||||
]
|
||||
@@ -23,6 +23,36 @@ class Client(BaseAuditModel):
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="clients",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
|
||||
# get old client if exists
|
||||
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kw)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_client and old_client.server_policy != self.server_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": self.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_client and old_client.workstation_policy != self.workstation_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": self.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -45,6 +75,7 @@ class Client(BaseAuditModel):
|
||||
"overdue_text_alert",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
.filter(site__client=self)
|
||||
.prefetch_related("agentchecks")
|
||||
@@ -87,6 +118,36 @@ class Site(BaseAuditModel):
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="sites",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
|
||||
# get old client if exists
|
||||
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(Site, self).save(*args, **kw)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_site and old_site.server_policy != self.server_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": self.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_site and old_site.workstation_policy != self.workstation_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": self.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -107,6 +168,7 @@ class Site(BaseAuditModel):
|
||||
"overdue_text_alert",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
.filter(site=self)
|
||||
.prefetch_related("agentchecks")
|
||||
|
||||
@@ -10,7 +10,7 @@ class SiteSerializer(ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def validate(self, val):
|
||||
if "|" in val["name"]:
|
||||
if "name" in val.keys() and "|" in val["name"]:
|
||||
raise ValidationError("Site name cannot contain the | character")
|
||||
|
||||
if self.context:
|
||||
@@ -36,7 +36,7 @@ class ClientSerializer(ModelSerializer):
|
||||
if len(self.context["site"]) > 255:
|
||||
raise ValidationError("Site name too long")
|
||||
|
||||
if "|" in val["name"]:
|
||||
if "name" in val.keys() and "|" in val["name"]:
|
||||
raise ValidationError("Client name cannot contain the | character")
|
||||
|
||||
return val
|
||||
|
||||
@@ -61,7 +61,8 @@ class GetAddClients(APIView):
|
||||
class GetUpdateDeleteClient(APIView):
|
||||
def put(self, request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
serializer = ClientSerializer(data=request.data, instance=client)
|
||||
|
||||
serializer = ClientSerializer(data=request.data, instance=client, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
@@ -106,7 +107,7 @@ class GetUpdateDeleteSite(APIView):
|
||||
def put(self, request, pk):
|
||||
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
serializer = SiteSerializer(instance=site, data=request.data)
|
||||
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
@@ -223,7 +224,7 @@ class GenerateAgent(APIView):
|
||||
f"GOARCH={goarch}",
|
||||
go_bin,
|
||||
"build",
|
||||
f"-ldflags=\"-X 'main.Inno={inno}'",
|
||||
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
|
||||
f"-X 'main.Api={api}'",
|
||||
f"-X 'main.Client={d.client.pk}'",
|
||||
f"-X 'main.Site={d.site.pk}'",
|
||||
|
||||
@@ -133,7 +133,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
fmt.Println("Installation starting.")
|
||||
cmd := exec.Command(tacrmm, cmdArgs...)
|
||||
|
||||
@@ -36,7 +36,7 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
|
||||
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
||||
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
|
||||
write-host ('Extracting...')
|
||||
Start-Sleep -s 10
|
||||
Start-Sleep -s 5
|
||||
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.4 on 2021-02-12 14:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0004_auto_20210212_1408'),
|
||||
('core', '0012_coresettings_check_history_prune_days'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coresettings',
|
||||
name='alert_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_alert_template', to='alerts.alerttemplate'),
|
||||
),
|
||||
]
|
||||
@@ -69,8 +69,17 @@ class CoreSettings(BaseAuditModel):
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="default_alert_template",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
|
||||
if not self.pk and CoreSettings.objects.exists():
|
||||
raise ValidationError("There can only be one CoreSettings instance")
|
||||
|
||||
@@ -83,7 +92,18 @@ class CoreSettings(BaseAuditModel):
|
||||
except:
|
||||
pass
|
||||
|
||||
return super(CoreSettings, self).save(*args, **kwargs)
|
||||
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_settings and old_settings.server_policy != self.server_policy:
|
||||
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_settings and old_settings.workstation_policy != self.workstation_policy:
|
||||
generate_all_agent_checks_task.delay(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Global Site Settings"
|
||||
@@ -124,18 +144,30 @@ class CoreSettings(BaseAuditModel):
|
||||
|
||||
return False
|
||||
|
||||
def send_mail(self, subject, body, test=False):
|
||||
def send_mail(self, subject, body, alert_template=None, test=False):
|
||||
|
||||
if not self.email_is_configured:
|
||||
if not alert_template and not self.email_is_configured:
|
||||
if test:
|
||||
return "Missing required fields (need at least 1 recipient)"
|
||||
return False
|
||||
|
||||
# override email from if alert_template is passed and is set
|
||||
if alert_template and alert_template.email_from:
|
||||
from_address = alert_template.email_from
|
||||
else:
|
||||
from_address = self.smtp_from_email
|
||||
|
||||
# override email recipients if alert_template is passed and is set
|
||||
if alert_template and alert_template.email_recipients:
|
||||
email_recipients = ", ".join(alert_template.email_recipients)
|
||||
else:
|
||||
email_recipients = ", ".join(self.email_alert_recipients)
|
||||
|
||||
try:
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = self.smtp_from_email
|
||||
msg["To"] = ", ".join(self.email_alert_recipients)
|
||||
msg["From"] = from_address
|
||||
msg["To"] = email_recipients
|
||||
msg.set_content(body)
|
||||
|
||||
with smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=20) as server:
|
||||
@@ -157,12 +189,18 @@ class CoreSettings(BaseAuditModel):
|
||||
else:
|
||||
return True
|
||||
|
||||
def send_sms(self, body):
|
||||
if not self.sms_is_configured:
|
||||
def send_sms(self, body, alert_template=None):
|
||||
if not alert_template and not self.sms_is_configured:
|
||||
return
|
||||
|
||||
# override email recipients if alert_template is passed and is set
|
||||
if alert_template and alert_template.text_recipients:
|
||||
text_recipients = alert_template.email_recipients
|
||||
else:
|
||||
text_recipients = self.sms_alert_recipients
|
||||
|
||||
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
|
||||
for num in self.sms_alert_recipients:
|
||||
for num in text_recipients:
|
||||
try:
|
||||
tw_client.messages.create(body=body, to=num, from_=self.twilio_number)
|
||||
except Exception as e:
|
||||
|
||||
@@ -43,18 +43,9 @@ def get_core_settings(request):
|
||||
@api_view(["PATCH"])
|
||||
def edit_settings(request):
|
||||
coresettings = CoreSettings.objects.first()
|
||||
old_server_policy = coresettings.server_policy
|
||||
old_workstation_policy = coresettings.workstation_policy
|
||||
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_settings = serializer.save()
|
||||
|
||||
# check if default policies changed
|
||||
if old_server_policy != new_settings.server_policy:
|
||||
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
|
||||
|
||||
if old_workstation_policy != new_settings.workstation_policy:
|
||||
generate_all_agent_checks_task.delay(mon_type="workstation", create_tasks=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -105,7 +96,7 @@ def server_maintenance(request):
|
||||
from agents.models import Agent
|
||||
from autotasks.tasks import remove_orphaned_win_tasks
|
||||
|
||||
agents = Agent.objects.only("pk", "last_seen", "overdue_time")
|
||||
agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time")
|
||||
online = [i for i in agents if i.status == "online"]
|
||||
for agent in online:
|
||||
remove_orphaned_win_tasks.delay(agent.pk)
|
||||
@@ -115,7 +106,6 @@ def server_maintenance(request):
|
||||
)
|
||||
|
||||
if request.data["action"] == "prune_db":
|
||||
from agents.models import AgentOutage
|
||||
from logs.models import AuditLog, PendingAction
|
||||
|
||||
if "prune_tables" not in request.data:
|
||||
@@ -123,11 +113,6 @@ def server_maintenance(request):
|
||||
|
||||
tables = request.data["prune_tables"]
|
||||
records_count = 0
|
||||
if "agent_outages" in tables:
|
||||
agentoutages = AgentOutage.objects.exclude(recovery_time=None)
|
||||
records_count += agentoutages.count()
|
||||
agentoutages.delete()
|
||||
|
||||
if "audit_logs" in tables:
|
||||
auditlogs = AuditLog.objects.filter(action="check_run")
|
||||
records_count += auditlogs.count()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from logs.models import AuditLog
|
||||
from model_bakery.recipe import Recipe
|
||||
from itertools import cycle
|
||||
|
||||
@@ -19,10 +18,10 @@ object_actions = ["add", "modify", "view", "delete"]
|
||||
agent_actions = ["remote_session", "execute_script", "execute_command"]
|
||||
login_actions = ["failed_login", "login"]
|
||||
|
||||
agent_logs = Recipe(AuditLog, action=cycle(agent_actions), object_type="agent")
|
||||
agent_logs = Recipe("logs.AuditLog", action=cycle(agent_actions), object_type="agent")
|
||||
|
||||
object_logs = Recipe(
|
||||
AuditLog, action=cycle(object_actions), object_type=cycle(object_types)
|
||||
"logs.AuditLog", action=cycle(object_actions), object_type=cycle(object_types)
|
||||
)
|
||||
|
||||
login_logs = Recipe(AuditLog, action=cycle(login_actions), object_type="user")
|
||||
login_logs = Recipe("logs.AuditLog", action=cycle(login_actions), object_type="user")
|
||||
|
||||
@@ -147,8 +147,8 @@ class TestAuditViews(TacticalTestCase):
|
||||
def test_options_filter(self):
|
||||
url = "/logs/auditlogs/optionsfilter/"
|
||||
|
||||
baker.make("agents.Agent", hostname=seq("AgentHostname"), _quantity=5)
|
||||
baker.make("agents.Agent", hostname=seq("Server"), _quantity=3)
|
||||
baker.make_recipe("agents.agent", hostname=seq("AgentHostname"), _quantity=5)
|
||||
baker.make_recipe("agents.agent", hostname=seq("Server"), _quantity=3)
|
||||
baker.make("accounts.User", username=seq("Username"), _quantity=7)
|
||||
baker.make("accounts.User", username=seq("soemthing"), _quantity=3)
|
||||
|
||||
@@ -194,7 +194,8 @@ class TestAuditViews(TacticalTestCase):
|
||||
|
||||
def test_all_pending_actions(self):
|
||||
url = "/logs/allpendingactions/"
|
||||
pending_actions = baker.make("logs.PendingAction", _quantity=6)
|
||||
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)
|
||||
|
||||
59
api/tacticalrmm/natsapi/tests.py
Normal file
59
api/tacticalrmm/natsapi/tests.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from model_bakery import baker
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class TestNatsAPIViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_nats_wmi(self):
|
||||
url = "/natsapi/wmi/"
|
||||
baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
|
||||
baker.make_recipe(
|
||||
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
|
||||
)
|
||||
baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.json()["agent_ids"]), 17)
|
||||
|
||||
def test_natscheckin_patch(self):
|
||||
from logs.models import PendingAction
|
||||
|
||||
url = "/natsapi/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()
|
||||
@@ -7,4 +7,8 @@ urlpatterns = [
|
||||
path("syncmesh/", views.SyncMeshNodeID.as_view()),
|
||||
path("winupdates/", views.NatsWinUpdates.as_view()),
|
||||
path("choco/", views.NatsChoco.as_view()),
|
||||
path("wmi/", views.NatsWMI.as_view()),
|
||||
path("offline/", views.OfflineAgents.as_view()),
|
||||
path("logcrash/", views.LogCrash.as_view()),
|
||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,8 @@ import asyncio
|
||||
import time
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
from typing import List
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -43,20 +45,31 @@ class NatsCheckIn(APIView):
|
||||
permission_classes = []
|
||||
|
||||
def patch(self, request):
|
||||
updated = False
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if pyver.parse(request.data["version"]) > pyver.parse(
|
||||
agent.version
|
||||
) or pyver.parse(request.data["version"]) == pyver.parse(
|
||||
settings.LATEST_AGENT_VER
|
||||
):
|
||||
updated = True
|
||||
agent.version = request.data["version"]
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["version", "last_seen"])
|
||||
|
||||
if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
|
||||
last_outage = agent.agentoutages.last()
|
||||
last_outage.recovery_time = djangotime.now()
|
||||
last_outage.save(update_fields=["recovery_time"])
|
||||
# change agent update pending status to completed if agent has just updated
|
||||
if (
|
||||
updated
|
||||
and agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists()
|
||||
):
|
||||
agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).update(status="completed")
|
||||
|
||||
if agent.overdue_email_alert:
|
||||
agent_recovery_email_task.delay(pk=last_outage.pk)
|
||||
if agent.overdue_text_alert:
|
||||
agent_recovery_sms_task.delay(pk=last_outage.pk)
|
||||
# handles any alerting actions
|
||||
agent.handle_alert(checkin=True)
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last()
|
||||
if recovery is not None:
|
||||
@@ -176,6 +189,7 @@ class NatsWinUpdates(APIView):
|
||||
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
|
||||
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request):
|
||||
@@ -199,6 +213,7 @@ class NatsWinUpdates(APIView):
|
||||
u.result = "failed"
|
||||
u.save(update_fields=["result"])
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
return Response("ok")
|
||||
|
||||
def post(self, request):
|
||||
@@ -233,4 +248,68 @@ class NatsWinUpdates(APIView):
|
||||
revision_number=update["revision_number"],
|
||||
).save()
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
|
||||
# more superseded updates cleanup
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
|
||||
for u in agent.winupdates.filter(
|
||||
date_installed__isnull=True, result="failed"
|
||||
).exclude(installed=True):
|
||||
u.delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SupersededWinUpdate(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
updates = agent.winupdates.filter(guid=request.data["guid"])
|
||||
for u in updates:
|
||||
u.delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class NatsWMI(APIView):
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
online: List[str] = [
|
||||
i.agent_id
|
||||
for i in agents
|
||||
if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
|
||||
]
|
||||
return Response({"agent_ids": online})
|
||||
|
||||
|
||||
class OfflineAgents(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
offline: List[str] = [
|
||||
i.agent_id for i in agents if i.has_nats and i.status != "online"
|
||||
]
|
||||
return Response({"agent_ids": offline})
|
||||
|
||||
|
||||
class LogCrash(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["last_seen"])
|
||||
return Response("ok")
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
amqp==5.0.2
|
||||
amqp==5.0.5
|
||||
asgiref==3.3.1
|
||||
asyncio-nats-client==0.11.4
|
||||
billiard==3.6.3.0
|
||||
celery==5.0.5
|
||||
certifi==2020.12.5
|
||||
cffi==1.14.4
|
||||
cffi==1.14.5
|
||||
chardet==4.0.0
|
||||
cryptography==3.3.1
|
||||
cryptography==3.4.4
|
||||
decorator==4.4.2
|
||||
Django==3.1.5
|
||||
django-cors-headers==3.6.0
|
||||
Django==3.1.6
|
||||
django-cors-headers==3.7.0
|
||||
django-rest-knox==4.1.0
|
||||
djangorestframework==3.12.2
|
||||
future==0.18.2
|
||||
idna==2.10
|
||||
kombu==5.0.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.2
|
||||
packaging==20.8
|
||||
psycopg2-binary==2.8.6
|
||||
pycparser==2.20
|
||||
pycryptodome==3.9.9
|
||||
pyotp==2.4.1
|
||||
pycryptodome==3.10.1
|
||||
pyotp==2.6.0
|
||||
pyparsing==2.4.7
|
||||
pytz==2020.5
|
||||
pytz==2021.1
|
||||
qrcode==6.1
|
||||
redis==3.5.3
|
||||
requests==2.25.1
|
||||
six==1.15.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.51.0
|
||||
urllib3==1.26.2
|
||||
twilio==6.52.0
|
||||
urllib3==1.26.3
|
||||
uWSGI==2.0.19.1
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from .models import Script
|
||||
from model_bakery.recipe import Recipe
|
||||
|
||||
script = Recipe(
|
||||
Script,
|
||||
"scripts.Script",
|
||||
name="Test Script",
|
||||
description="Test Desc",
|
||||
shell="cmd",
|
||||
|
||||
@@ -193,6 +193,13 @@
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "TRMM Defender Exclusions",
|
||||
"description": "Windows Defender Exclusions for Tactical RMM",
|
||||
"shell": "cmd"
|
||||
"shell": "powershell"
|
||||
},
|
||||
{
|
||||
"filename": "Display_Message_To_User.ps1",
|
||||
"submittedBy": "https://github.com/bradhawkins85",
|
||||
"name": "Display Message To User",
|
||||
"description": "Displays a popup message to the currently logged on user",
|
||||
"shell": "powershell"
|
||||
}
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-10 21:45
|
||||
|
||||
from django.db import migrations
|
||||
from django.conf import settings
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def move_scripts_to_db(apps, schema_editor):
|
||||
print("")
|
||||
Script = apps.get_model("scripts", "Script")
|
||||
for script in Script.objects.all():
|
||||
if not script.script_type == "builtin":
|
||||
|
||||
if script.filename:
|
||||
filepath = f"{settings.SCRIPTS_DIR}/userdefined/{script.filename}"
|
||||
else:
|
||||
print(f"No filename on script found. Skipping")
|
||||
continue
|
||||
|
||||
# test if file exists
|
||||
if os.path.exists(filepath):
|
||||
print(f"Found script {script.name}. Importing code.")
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
script_bytes = f.read().decode("utf-8").encode("ascii", "ignore")
|
||||
script.code_base64 = base64.b64encode(script_bytes).decode("ascii")
|
||||
script.save(update_fields=["code_base64"])
|
||||
else:
|
||||
print(
|
||||
f"Script file {script.name} was not found on the disk. You will need to edit the script in the UI"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scripts", "0005_auto_20201207_1606"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(move_scripts_to_db, migrations.RunPython.noop)]
|
||||
@@ -72,6 +72,7 @@ class Script(BaseAuditModel):
|
||||
i.name = script["name"]
|
||||
i.description = script["description"]
|
||||
i.category = "Community"
|
||||
i.shell = script["shell"]
|
||||
|
||||
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
|
||||
script_bytes = (
|
||||
@@ -80,7 +81,13 @@ class Script(BaseAuditModel):
|
||||
i.code_base64 = base64.b64encode(script_bytes).decode("ascii")
|
||||
|
||||
i.save(
|
||||
update_fields=["name", "description", "category", "code_base64"]
|
||||
update_fields=[
|
||||
"name",
|
||||
"description",
|
||||
"category",
|
||||
"code_base64",
|
||||
"shell",
|
||||
]
|
||||
)
|
||||
else:
|
||||
print(f"Adding new community script: {script['name']}")
|
||||
|
||||
@@ -195,15 +195,21 @@ class TestScriptViews(TacticalTestCase):
|
||||
info = json.load(f)
|
||||
|
||||
for script in info:
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(scripts_dir, script["filename"]))
|
||||
)
|
||||
fn: str = script["filename"]
|
||||
self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn)))
|
||||
self.assertTrue(script["filename"])
|
||||
self.assertTrue(script["name"])
|
||||
self.assertTrue(script["description"])
|
||||
self.assertTrue(script["shell"])
|
||||
self.assertIn(script["shell"], valid_shells)
|
||||
|
||||
if fn.endswith(".ps1"):
|
||||
self.assertEqual(script["shell"], "powershell")
|
||||
elif fn.endswith(".bat"):
|
||||
self.assertEqual(script["shell"], "cmd")
|
||||
elif fn.endswith(".py"):
|
||||
self.assertEqual(script["shell"], "python")
|
||||
|
||||
def test_load_community_scripts(self):
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
|
||||
|
||||
@@ -8,6 +8,7 @@ from agents.models import Agent
|
||||
class TestServiceViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_default_services(self):
|
||||
url = "/services/defaultservices/"
|
||||
|
||||
@@ -8,6 +8,7 @@ from .models import ChocoLog
|
||||
class TestSoftwareViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_chocos_get(self):
|
||||
url = "/software/chocos/"
|
||||
@@ -63,6 +64,9 @@ class TestSoftwareViews(TacticalTestCase):
|
||||
|
||||
|
||||
class TestSoftwareTasks(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_install_program(self, nats_cmd):
|
||||
from .tasks import install_program
|
||||
|
||||
@@ -29,30 +29,10 @@ app.conf.beat_schedule = {
|
||||
"task": "winupdate.tasks.check_agent_update_schedule_task",
|
||||
"schedule": crontab(minute=5, hour="*"),
|
||||
},
|
||||
"agents-checkinfull": {
|
||||
"task": "agents.tasks.check_in_task",
|
||||
"schedule": crontab(minute="*/24"),
|
||||
},
|
||||
"agent-auto-update": {
|
||||
"task": "agents.tasks.auto_self_agent_update_task",
|
||||
"schedule": crontab(minute=35, hour="*"),
|
||||
},
|
||||
"agents-sync": {
|
||||
"task": "agents.tasks.sync_sysinfo_task",
|
||||
"schedule": crontab(minute=55, hour="*"),
|
||||
},
|
||||
"get-wmi": {
|
||||
"task": "agents.tasks.get_wmi_task",
|
||||
"schedule": crontab(minute="*/18"),
|
||||
},
|
||||
"check-agentservice": {
|
||||
"task": "agents.tasks.monitor_agents_task",
|
||||
"schedule": crontab(minute="*/15"),
|
||||
},
|
||||
"remove-salt": {
|
||||
"task": "agents.tasks.remove_salt_task",
|
||||
"schedule": crontab(minute=14, hour="*/2"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +46,8 @@ def setup_periodic_tasks(sender, **kwargs):
|
||||
|
||||
from agents.tasks import agent_outages_task
|
||||
from core.tasks import core_maintenance_tasks
|
||||
from alerts.tasks import unsnooze_alerts, periodic_alert_notifications
|
||||
|
||||
sender.add_periodic_task(60.0, agent_outages_task.s())
|
||||
sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s())
|
||||
sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s())
|
||||
|
||||
@@ -15,20 +15,20 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.4.1"
|
||||
TRMM_VERSION = "0.4.8"
|
||||
|
||||
# bump this version everytime vue code is changed
|
||||
# to alert user they need to manually refresh their browser
|
||||
APP_VER = "0.0.108"
|
||||
APP_VER = "0.0.112"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.4.0"
|
||||
LATEST_AGENT_VER = "1.4.5"
|
||||
|
||||
MESH_VER = "0.7.54"
|
||||
MESH_VER = "0.7.68"
|
||||
|
||||
# for the update script, bump when need to recreate venv or npm install
|
||||
PIP_VER = "7"
|
||||
NPM_VER = "7"
|
||||
PIP_VER = "9"
|
||||
NPM_VER = "8"
|
||||
|
||||
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
|
||||
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import string
|
||||
import subprocess
|
||||
import time
|
||||
import pytz
|
||||
from typing import List, Dict
|
||||
from loguru import logger
|
||||
|
||||
@@ -29,6 +30,12 @@ WEEK_DAYS = {
|
||||
}
|
||||
|
||||
|
||||
def get_default_timezone():
|
||||
from core.models import CoreSettings
|
||||
|
||||
return pytz.timezone(CoreSettings.objects.first().default_time_zone)
|
||||
|
||||
|
||||
def get_bit_days(days: List[str]) -> int:
|
||||
bit_days = 0
|
||||
for day in days:
|
||||
|
||||
@@ -19,8 +19,11 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
def auto_approve_updates_task():
|
||||
# scheduled task that checks and approves updates daily
|
||||
|
||||
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
|
||||
agents = Agent.objects.only(
|
||||
"pk", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
for agent in agents:
|
||||
agent.delete_superseded_updates()
|
||||
try:
|
||||
agent.approve_updates()
|
||||
except:
|
||||
@@ -43,7 +46,9 @@ def auto_approve_updates_task():
|
||||
@app.task
|
||||
def check_agent_update_schedule_task():
|
||||
# scheduled task that installs updates on agents if enabled
|
||||
agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
|
||||
agents = Agent.objects.only(
|
||||
"pk", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
online = [
|
||||
i
|
||||
for i in agents
|
||||
@@ -53,6 +58,7 @@ def check_agent_update_schedule_task():
|
||||
]
|
||||
|
||||
for agent in online:
|
||||
agent.delete_superseded_updates()
|
||||
install = False
|
||||
patch_policy = agent.get_patch_policy()
|
||||
|
||||
@@ -126,6 +132,11 @@ def bulk_install_updates_task(pks: List[int]) -> None:
|
||||
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
|
||||
for chunk in chunks:
|
||||
for agent in chunk:
|
||||
agent.delete_superseded_updates()
|
||||
try:
|
||||
agent.approve_updates()
|
||||
except:
|
||||
pass
|
||||
nats_data = {
|
||||
"func": "installwinupdates",
|
||||
"guids": agent.get_approved_update_guids(),
|
||||
@@ -142,6 +153,7 @@ def bulk_check_for_updates_task(pks: List[int]) -> None:
|
||||
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
|
||||
for chunk in chunks:
|
||||
for agent in chunk:
|
||||
agent.delete_superseded_updates()
|
||||
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
|
||||
time.sleep(0.05)
|
||||
time.sleep(15)
|
||||
|
||||
@@ -88,7 +88,8 @@ class TestWinUpdateViews(TacticalTestCase):
|
||||
|
||||
def test_edit_policy(self):
|
||||
url = "/winupdate/editpolicy/"
|
||||
winupdate = baker.make("winupdate.WinUpdate")
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
winupdate = baker.make("winupdate.WinUpdate", agent=agent)
|
||||
|
||||
invalid_data = {"pk": 500, "policy": "inherit"}
|
||||
# test a call where winupdate doesn't exist
|
||||
|
||||
@@ -22,6 +22,7 @@ def get_win_updates(request, pk):
|
||||
@api_view()
|
||||
def run_update_scan(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
if pyver.parse(agent.version) < pyver.parse("1.3.0"):
|
||||
return notify_error("Requires agent version 1.3.0 or greater")
|
||||
|
||||
@@ -32,9 +33,11 @@ def run_update_scan(request, pk):
|
||||
@api_view()
|
||||
def install_updates(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
if pyver.parse(agent.version) < pyver.parse("1.3.0"):
|
||||
return notify_error("Requires agent version 1.3.0 or greater")
|
||||
|
||||
agent.approve_updates()
|
||||
nats_data = {
|
||||
"func": "installwinupdates",
|
||||
"guids": agent.get_approved_update_guids(),
|
||||
|
||||
@@ -7,8 +7,8 @@ jobs:
|
||||
displayName: "Setup"
|
||||
strategy:
|
||||
matrix:
|
||||
Ubuntu20:
|
||||
AGENT_NAME: "rmm-ubu20"
|
||||
Debian10:
|
||||
AGENT_NAME: "azpipelines-deb10"
|
||||
|
||||
pool:
|
||||
name: linux-vms
|
||||
@@ -23,11 +23,11 @@ jobs:
|
||||
|
||||
rm -rf /myagent/_work/1/s/api/env
|
||||
cd /myagent/_work/1/s/api
|
||||
python3 -m venv env
|
||||
python3.8 -m venv env
|
||||
source env/bin/activate
|
||||
cd /myagent/_work/1/s/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
|
||||
pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
|
||||
pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
|
||||
displayName: "Install Python Dependencies"
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- script: |
|
||||
cd /myagent/_work/1/s/api
|
||||
source env/bin/activate
|
||||
black --check tacticalrmm
|
||||
black --exclude migrations/ --check tacticalrmm
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
18
backup.sh
18
backup.sh
@@ -1,6 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="7"
|
||||
#####################################################
|
||||
|
||||
POSTGRES_USER="changeme"
|
||||
POSTGRES_PW="hunter2"
|
||||
|
||||
#####################################################
|
||||
|
||||
SCRIPT_VERSION="8"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -24,13 +31,6 @@ if [ $EUID -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#####################################################
|
||||
|
||||
POSTGRES_USER="changeme"
|
||||
POSTGRES_PW="hunter2"
|
||||
|
||||
#####################################################
|
||||
|
||||
if [[ "$POSTGRES_USER" == "changeme" || "$POSTGRES_PW" == "hunter2" ]]; then
|
||||
printf >&2 "${RED}You must change the postgres username/password at the top of this file.${NC}\n"
|
||||
printf >&2 "${RED}Check the github readme for where to find them.${NC}\n"
|
||||
@@ -43,7 +43,7 @@ if [ ! -d /rmmbackups ]; then
|
||||
fi
|
||||
|
||||
if [ -d /meshcentral/meshcentral-backup ]; then
|
||||
rm -f /meshcentral/meshcentral-backup/*
|
||||
rm -rf /meshcentral/meshcentral-backup/*
|
||||
fi
|
||||
|
||||
if [ -d /meshcentral/meshcentral-coredumps ]; then
|
||||
|
||||
8
go.mod
8
go.mod
@@ -3,14 +3,10 @@ module github.com/wh1te909/tacticalrmm
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.4.0
|
||||
github.com/josephspurrier/goversioninfo v1.2.0
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/nats-io/nats.go v1.10.1-0.20210107160453-a133396829fc
|
||||
github.com/ugorji/go/codec v1.2.2
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5
|
||||
github.com/ugorji/go/codec v1.2.3
|
||||
github.com/wh1te909/rmmagent v1.4.3
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
57
go.sum
57
go.sum
@@ -5,12 +5,11 @@ github.com/capnspacehook/taskmaster v0.0.0-20201022195506-c2d8b114cec0/go.mod h1
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elastic/go-sysinfo v1.4.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
|
||||
github.com/elastic/go-sysinfo v1.5.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
|
||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
|
||||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k=
|
||||
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -37,7 +36,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
|
||||
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
@@ -68,14 +66,15 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs=
|
||||
github.com/rickb777/date v1.14.3/go.mod h1:mes+vf4wqTD6l4zgZh4Z5TQkrLA57dpuzEGVeTk/XSc=
|
||||
github.com/rickb777/date v1.15.3/go.mod h1:+spwdRnUrpqbYLOmRM6y8FbQMXwpNwHrNcWuOUipge4=
|
||||
github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
|
||||
github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
|
||||
github.com/shirou/gopsutil/v3 v3.20.12/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -83,32 +82,17 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tc-hib/goversioninfo v0.0.0-20200813185747-90ffbaa484a7/go.mod h1:NaPIGx19A2KXQEoek0x88NbM0lNgRooZS0xmrETzcjI=
|
||||
github.com/tc-hib/rsrc v0.9.1/go.mod h1:JGDB/TLOdMTvEEvjv3yetUTFnjXWYLbZDDeH4BTXG/8=
|
||||
github.com/tc-hib/rsrc v0.9.2/go.mod h1:vUZqBwu0vX+ueZH/D5wEvihBZfON5BrWCg6Orbfq7A4=
|
||||
github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc=
|
||||
github.com/ugorji/go v1.2.2 h1:60ZHIOcsJlo3bJm9CbTVu7OSqT2mxaEmyQbK2NwCkn0=
|
||||
github.com/ugorji/go v1.2.2/go.mod h1:bitgyERdV7L7Db/Z5gfd5v2NQMNhhiFiZwpgMw2SP7k=
|
||||
github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs=
|
||||
github.com/ugorji/go/codec v1.2.2 h1:08Gah8d+dXj4cZNUHhtuD/S4PXD5WpVbj5B8/ClELAQ=
|
||||
github.com/ugorji/go/codec v1.2.2/go.mod h1:OM8g7OAy52uYl3Yk+RE/3AS1nXFn1Wh4PPLtupCxbuU=
|
||||
github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk=
|
||||
github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A=
|
||||
github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0=
|
||||
github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc=
|
||||
github.com/wh1te909/go-win64api v0.0.0-20201021040544-8fba2a0fc3d0/go.mod h1:cfD5/vNQFm5PD5Q32YYYBJ6VIs9etzp8CJ9dinUcpUA=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111092134-7c83da579caa h1:ZV7qIUJ5M3HDFLi3bun6a2A5+g9DoThbLWI7egBYYkQ=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111092134-7c83da579caa/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111205455-e6620a17aebe h1:xsutMbsAJL2xTvE119BVyK4RdBWx1IBvC7azoEpioEE=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111205455-e6620a17aebe/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210112033642-9b310c2c7f53 h1:Q47sibbW09BWaQoPZQTzblGd+rnNIc3W8W/jOYbMe10=
|
||||
github.com/wh1te909/rmmagent v1.1.13-0.20210112033642-9b310c2c7f53/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.2.0 h1:dM/juD7k6Oa0lEKsvbNPgjc1wVC6uQtNzQoIqVuuxSQ=
|
||||
github.com/wh1te909/rmmagent v1.2.0/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210118235958-bd6606570a6f h1:lhcD2yJauZ8TyYCxYvSv/CPnUhiTrxwydPTESfPkyuc=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210118235958-bd6606570a6f/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119030741-08ec2f919198 h1:lPxk5AEr/2y8txGtvbQgW0rofZ7RFaJBYmS8rLIxoVQ=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119030741-08ec2f919198/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119225811-d3b8795ce1d7 h1:ctMUmZtlI2dH1WCndTFPOueWgYd18n+onYsnMKT/lns=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119225811-d3b8795ce1d7/go.mod h1:TG09pCLQZcN5jyrokVty3eHImponjh5nMmifru9RPeY=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5 h1:md2uqZE2Too7mRvWCvA7vDpdpFP1bMEKWAfrIa0ARiA=
|
||||
github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5/go.mod h1:TG09pCLQZcN5jyrokVty3eHImponjh5nMmifru9RPeY=
|
||||
github.com/wh1te909/rmmagent v1.4.2 h1:noG/ELSue3d6UF7o0gp1ty0DpGbfrVz0VG6NEQm9kGM=
|
||||
github.com/wh1te909/rmmagent v1.4.2/go.mod h1:mcI27szhAGjAQzhX8eCsluE5670kUAyi3tJVJa6LNTo=
|
||||
github.com/wh1te909/rmmagent v1.4.3-0.20210202083714-12142d6dfd1f h1:9r7AFleOMiVs8AiAsUezau3GVL22bqbUDz6qlRUIMFM=
|
||||
github.com/wh1te909/rmmagent v1.4.3-0.20210202083714-12142d6dfd1f/go.mod h1:mcI27szhAGjAQzhX8eCsluE5670kUAyi3tJVJa6LNTo=
|
||||
github.com/wh1te909/rmmagent v1.4.3 h1:9jbze6oqUBe8HJedXTrZ5i7C8dDEQt+Ifmp9am4HtZQ=
|
||||
github.com/wh1te909/rmmagent v1.4.3/go.mod h1:mcI27szhAGjAQzhX8eCsluE5670kUAyi3tJVJa6LNTo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -117,11 +101,10 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNm
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
@@ -133,6 +116,7 @@ golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -141,9 +125,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200622182413-4b0db7f3f76b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8=
|
||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -169,6 +153,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
|
||||
17
install.sh
17
install.sh
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="34"
|
||||
SCRIPT_VERSION="38"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
|
||||
|
||||
sudo apt install -y curl wget
|
||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -185,9 +185,9 @@ print_green 'Installing golang'
|
||||
|
||||
sudo mkdir -p /usr/local/rmmgo
|
||||
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
|
||||
wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz -P ${go_tmp}
|
||||
wget https://golang.org/dl/go1.15.8.linux-amd64.tar.gz -P ${go_tmp}
|
||||
|
||||
tar -xzf ${go_tmp}/go1.15.6.linux-amd64.tar.gz -C ${go_tmp}
|
||||
tar -xzf ${go_tmp}/go1.15.8.linux-amd64.tar.gz -C ${go_tmp}
|
||||
|
||||
sudo mv ${go_tmp}/go /usr/local/rmmgo/
|
||||
rm -rf ${go_tmp}
|
||||
@@ -375,7 +375,7 @@ python3 -m venv env
|
||||
source /rmm/api/env/bin/activate
|
||||
cd /rmm/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
|
||||
pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
|
||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
|
||||
python manage.py migrate
|
||||
python manage.py collectstatic --no-input
|
||||
@@ -398,19 +398,17 @@ read -n 1 -s -r -p "Press any key to continue..."
|
||||
|
||||
uwsgini="$(cat << EOF
|
||||
[uwsgi]
|
||||
|
||||
# logto = /rmm/api/tacticalrmm/tacticalrmm/private/log/uwsgi.log
|
||||
chdir = /rmm/api/tacticalrmm
|
||||
module = tacticalrmm.wsgi
|
||||
home = /rmm/api/env
|
||||
master = true
|
||||
processes = 6
|
||||
threads = 6
|
||||
enable-threads = True
|
||||
enable-threads = true
|
||||
socket = /rmm/api/tacticalrmm/tacticalrmm.sock
|
||||
harakiri = 300
|
||||
chmod-socket = 660
|
||||
# clear environment on exit
|
||||
buffer-size = 65535
|
||||
vacuum = true
|
||||
die-on-term = true
|
||||
max-requests = 500
|
||||
@@ -788,6 +786,7 @@ sleep 5
|
||||
MESHEXE=$(node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} GenerateInviteLink --group TacticalRMM --hours 8)
|
||||
|
||||
sudo systemctl enable nats.service
|
||||
sudo systemctl enable natsapi.service
|
||||
cd /rmm/api/tacticalrmm
|
||||
source /rmm/api/env/bin/activate
|
||||
python manage.py initial_db_setup
|
||||
|
||||
2
main.go
2
main.go
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/wh1te909/tacticalrmm/natsapi"
|
||||
)
|
||||
|
||||
var version = "1.0.2"
|
||||
var version = "1.0.7"
|
||||
|
||||
func main() {
|
||||
ver := flag.Bool("version", false, "Prints version")
|
||||
|
||||
@@ -75,6 +75,9 @@ func Listen(apihost, natshost, version string, debug bool) {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
go getWMI(rClient, nc)
|
||||
go monitorAgents(rClient, nc)
|
||||
|
||||
nc.Subscribe("*", func(msg *nats.Msg) {
|
||||
var mh codec.MsgpackHandle
|
||||
mh.RawToString = true
|
||||
@@ -158,6 +161,13 @@ func Listen(apihost, natshost, version string, debug bool) {
|
||||
rClient.R().SetBody(p).Patch("/winupdates/")
|
||||
}
|
||||
}()
|
||||
case "superseded":
|
||||
go func() {
|
||||
var p *rmm.SupersededUpdate
|
||||
if err := dec.Decode(&p); err == nil {
|
||||
rClient.R().SetBody(p).Post("/superseded/")
|
||||
}
|
||||
}()
|
||||
case "needsreboot":
|
||||
go func() {
|
||||
var p *rmm.AgentNeedsReboot
|
||||
|
||||
Binary file not shown.
76
natsapi/tasks.go
Normal file
76
natsapi/tasks.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
nats "github.com/nats-io/nats.go"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
func monitorAgents(c *resty.Client, nc *nats.Conn) {
|
||||
var payload, recPayload []byte
|
||||
var mh codec.MsgpackHandle
|
||||
mh.RawToString = true
|
||||
ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
|
||||
ret.Encode(map[string]string{"func": "ping"})
|
||||
|
||||
rec := codec.NewEncoderBytes(&recPayload, new(codec.MsgpackHandle))
|
||||
rec.Encode(Recovery{
|
||||
Func: "recover",
|
||||
Data: map[string]string{"mode": "tacagent"},
|
||||
})
|
||||
|
||||
tick := time.NewTicker(10 * time.Minute)
|
||||
for range tick.C {
|
||||
var wg sync.WaitGroup
|
||||
agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/offline/")
|
||||
ids := agentids.Result().(*AgentIDS).IDs
|
||||
wg.Add(len(ids))
|
||||
var resp string
|
||||
|
||||
for _, id := range ids {
|
||||
go func(id string, nc *nats.Conn, wg *sync.WaitGroup, c *resty.Client) {
|
||||
defer wg.Done()
|
||||
out, err := nc.Request(id, payload, 1*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dec := codec.NewDecoderBytes(out.Data, &mh)
|
||||
if err := dec.Decode(&resp); err == nil {
|
||||
// if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
|
||||
// then tacticalagent service is hung. forcefully restart it
|
||||
if resp == "pong" {
|
||||
nc.Publish(id, recPayload)
|
||||
p := map[string]string{"agentid": id}
|
||||
c.R().SetBody(p).Post("/logcrash/")
|
||||
}
|
||||
}
|
||||
}(id, nc, &wg, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getWMI(c *resty.Client, nc *nats.Conn) {
|
||||
var payload []byte
|
||||
var mh codec.MsgpackHandle
|
||||
mh.RawToString = true
|
||||
ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
|
||||
ret.Encode(map[string]string{"func": "wmi"})
|
||||
|
||||
tick := time.NewTicker(18 * time.Minute)
|
||||
for range tick.C {
|
||||
agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/wmi/")
|
||||
ids := agentids.Result().(*AgentIDS).IDs
|
||||
chunks := makeChunks(ids, 40)
|
||||
|
||||
for _, id := range chunks {
|
||||
for _, chunk := range id {
|
||||
nc.Publish(chunk, payload)
|
||||
time.Sleep(time.Duration(randRange(50, 400)) * time.Millisecond)
|
||||
}
|
||||
time.Sleep(15 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user