Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5868778a | ||
|
|
a10b8dab9b | ||
|
|
92f4f7ef59 | ||
|
|
31257bd5cb | ||
|
|
bb6510862f | ||
|
|
797ecf0780 | ||
|
|
f9536dc67f | ||
|
|
e8b95362af | ||
|
|
bdc39ad4ec | ||
|
|
4a202c5585 | ||
|
|
3c6b321f73 | ||
|
|
cb29b52799 | ||
|
|
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 |
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN groupadd -g 1000 tactical && \
|
||||
useradd -u 1000 -g 1000 tactical
|
||||
|
||||
# Copy Go Files
|
||||
COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go
|
||||
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
|
||||
|
||||
# Copy Dev python reqs
|
||||
COPY ./requirements.txt /
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,110 @@ 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().values_list("pk", flat=True)
|
||||
or site.pk in template.excluded_sites.all().values_list("pk", flat=True)
|
||||
or self.pk
|
||||
in template.excluded_agents.all()
|
||||
.only("pk")
|
||||
.values_list("pk", flat=True)
|
||||
):
|
||||
continue
|
||||
|
||||
# check if template is excluding desktops
|
||||
if self.monitoring_type == "workstation" and template.exclude_desktops:
|
||||
continue
|
||||
|
||||
# check if template is excluding servers
|
||||
elif self.monitoring_type == "server" and template.exclude_servers:
|
||||
continue
|
||||
else:
|
||||
return template
|
||||
|
||||
# no alert templates found or agent has been excluded
|
||||
return None
|
||||
|
||||
def generate_checks_from_policies(self):
|
||||
from automation.models import Policy
|
||||
|
||||
@@ -520,73 +704,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"),
|
||||
|
||||
@@ -37,7 +37,12 @@ class AgentSerializer(serializers.ModelSerializer):
|
||||
class AgentOverdueActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = ["pk", "overdue_email_alert", "overdue_text_alert"]
|
||||
fields = [
|
||||
"pk",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
]
|
||||
|
||||
|
||||
class AgentTableSerializer(serializers.ModelSerializer):
|
||||
@@ -50,6 +55,21 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
site_name = serializers.ReadOnlyField(source="site.name")
|
||||
logged_username = serializers.SerializerMethodField()
|
||||
italic = serializers.SerializerMethodField()
|
||||
policy = serializers.ReadOnlyField(source="policy.id")
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
alert_template = obj.get_alert_template()
|
||||
|
||||
if not alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.agent_always_email,
|
||||
"always_text": alert_template.agent_always_text,
|
||||
"always_alert": alert_template.agent_always_alert,
|
||||
}
|
||||
|
||||
def get_pending_actions(self, obj):
|
||||
return obj.pendingactions.filter(status="pending").count()
|
||||
@@ -77,6 +97,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
model = Agent
|
||||
fields = [
|
||||
"id",
|
||||
"alert_template",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"site_name",
|
||||
@@ -89,12 +110,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
|
||||
|
||||
@@ -120,10 +143,12 @@ class AgentEditSerializer(serializers.ModelSerializer):
|
||||
"timezone",
|
||||
"check_interval",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"all_timezones",
|
||||
"winupdatepolicy",
|
||||
"policy",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ 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
|
||||
|
||||
@@ -18,9 +20,18 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
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
|
||||
@@ -36,55 +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"
|
||||
else:
|
||||
logger.warning(
|
||||
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to update."
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -114,66 +108,94 @@ def auto_self_agent_update_task() -> None:
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_outage_email_task(pk):
|
||||
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 agent_recovery_email_task(pk: int) -> str:
|
||||
from alerts.models import Alert
|
||||
|
||||
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
|
||||
@@ -192,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
|
||||
@@ -237,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)
|
||||
|
||||
@@ -334,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")
|
||||
@@ -407,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,
|
||||
@@ -680,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/"
|
||||
@@ -690,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,
|
||||
@@ -762,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(
|
||||
@@ -790,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",
|
||||
@@ -810,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
|
||||
@@ -59,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")
|
||||
|
||||
|
||||
@@ -93,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")
|
||||
|
||||
@@ -244,6 +243,7 @@ class AgentsTableList(generics.ListAPIView):
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
@@ -292,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",
|
||||
@@ -321,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",
|
||||
@@ -559,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",
|
||||
@@ -702,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")
|
||||
@@ -726,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()
|
||||
@@ -853,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),
|
||||
),
|
||||
]
|
||||
72
api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
Normal file
72
api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-17 17:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0005_auto_20210212_1745'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='alerttemplate',
|
||||
name='agent_include_desktops',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alerttemplate',
|
||||
name='exclude_servers',
|
||||
field=models.BooleanField(blank=True, default=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alerttemplate',
|
||||
name='exclude_workstations',
|
||||
field=models.BooleanField(blank=True, default=False, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='agent_always_alert',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='agent_always_email',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='agent_always_text',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='check_always_alert',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='check_always_email',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='check_always_text',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='task_always_alert',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='task_always_email',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alerttemplate',
|
||||
name='task_always_text',
|
||||
field=models.BooleanField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
||||
@@ -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,256 @@ 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_always_email = BooleanField(null=True, blank=True, default=None)
|
||||
agent_always_text = BooleanField(null=True, blank=True, default=None)
|
||||
agent_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
# check alert settings
|
||||
check_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
check_always_email = BooleanField(null=True, blank=True, default=None)
|
||||
check_always_text = BooleanField(null=True, blank=True, default=None)
|
||||
check_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
# task alert settings
|
||||
task_email_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_text_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_dashboard_alert_severity = ArrayField(
|
||||
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
|
||||
task_always_email = BooleanField(null=True, blank=True, default=None)
|
||||
task_always_text = BooleanField(null=True, blank=True, default=None)
|
||||
task_always_alert = BooleanField(null=True, blank=True, default=None)
|
||||
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
|
||||
|
||||
# exclusion settings
|
||||
exclude_workstations = BooleanField(null=True, blank=True, default=False)
|
||||
exclude_servers = BooleanField(null=True, blank=True, default=False)
|
||||
|
||||
excluded_sites = models.ManyToManyField(
|
||||
"clients.Site", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
excluded_clients = models.ManyToManyField(
|
||||
"clients.Client", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
excluded_agents = models.ManyToManyField(
|
||||
"agents.Agent", related_name="alert_exclusions", blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def has_agent_settings(self) -> bool:
|
||||
return (
|
||||
self.agent_email_on_resolved
|
||||
or self.agent_text_on_resolved
|
||||
or self.agent_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 CheckRunnerGetSerializer
|
||||
from agents.serializers import WinAgentSerializer
|
||||
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")
|
||||
|
||||
|
||||
@@ -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,76 +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"])
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
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."
|
||||
)
|
||||
|
||||
agent.delete_superseded_updates()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class SysInfo(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -377,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 """
|
||||
|
||||
@@ -464,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)
|
||||
|
||||
@@ -14,6 +14,24 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
assigned_check = CheckSerializer(read_only=True)
|
||||
schedule = serializers.ReadOnlyField()
|
||||
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
|
||||
if obj.agent:
|
||||
alert_template = obj.agent.get_alert_template()
|
||||
else:
|
||||
alert_template = None
|
||||
|
||||
if not alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.task_always_email,
|
||||
"always_text": alert_template.task_always_text,
|
||||
"always_alert": alert_template.task_always_alert,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
|
||||
@@ -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,20 +639,13 @@ 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
|
||||
|
||||
@@ -478,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,
|
||||
@@ -530,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":
|
||||
|
||||
@@ -556,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()}"
|
||||
|
||||
@@ -601,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"
|
||||
@@ -613,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)
|
||||
@@ -642,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):
|
||||
|
||||
@@ -20,6 +20,23 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
assigned_task = serializers.SerializerMethodField()
|
||||
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
|
||||
history_info = serializers.ReadOnlyField()
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
|
||||
def get_alert_template(self, obj):
|
||||
if obj.agent:
|
||||
alert_template = obj.agent.get_alert_template()
|
||||
else:
|
||||
alert_template = None
|
||||
|
||||
if not alert_template:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"name": alert_template.name,
|
||||
"always_email": alert_template.check_always_email,
|
||||
"always_text": alert_template.check_always_text,
|
||||
"always_alert": alert_template.check_always_alert,
|
||||
}
|
||||
|
||||
## Change to return only array of tasks after 9/25/2020
|
||||
def get_assigned_task(self, obj):
|
||||
@@ -40,19 +57,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 +108,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 +132,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
|
||||
|
||||
|
||||
@@ -118,8 +179,6 @@ class CheckRunnerGetSerializer(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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -29,23 +29,5 @@ class Command(BaseCommand):
|
||||
self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
|
||||
)
|
||||
|
||||
# install go
|
||||
if not os.path.exists("/usr/local/rmmgo/"):
|
||||
self.stdout.write(self.style.SUCCESS("Installing golang"))
|
||||
subprocess.run("sudo mkdir -p /usr/local/rmmgo", shell=True)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
r = subprocess.run(
|
||||
f"wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P {tmpdir}",
|
||||
shell=True,
|
||||
)
|
||||
|
||||
gotar = os.path.join(tmpdir, "go1.15.5.linux-amd64.tar.gz")
|
||||
|
||||
subprocess.run(f"tar -xzf {gotar} -C {tmpdir}", shell=True)
|
||||
|
||||
gofolder = os.path.join(tmpdir, "go")
|
||||
subprocess.run(f"sudo mv {gofolder} /usr/local/rmmgo/", shell=True)
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
# load community scripts into the db
|
||||
Script.load_community_scripts()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,3 +21,39 @@ class TestNatsAPIViews(TacticalTestCase):
|
||||
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()
|
||||
|
||||
@@ -10,4 +10,5 @@ urlpatterns = [
|
||||
path("wmi/", views.NatsWMI.as_view()),
|
||||
path("offline/", views.OfflineAgents.as_view()),
|
||||
path("logcrash/", views.LogCrash.as_view()),
|
||||
path("superseded/", views.SupersededWinUpdate.as_view()),
|
||||
]
|
||||
|
||||
@@ -45,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:
|
||||
@@ -238,6 +249,27 @@ class NatsWinUpdates(APIView):
|
||||
).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")
|
||||
|
||||
|
||||
@@ -248,7 +280,7 @@ class NatsWMI(APIView):
|
||||
|
||||
def get(self, request):
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time"
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
online: List[str] = [
|
||||
i.agent_id
|
||||
@@ -264,7 +296,7 @@ class OfflineAgents(APIView):
|
||||
|
||||
def get(self, request):
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time"
|
||||
"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"
|
||||
@@ -278,9 +310,6 @@ class LogCrash(APIView):
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
|
||||
logger.info(
|
||||
f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
|
||||
)
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["last_seen"])
|
||||
return Response("ok")
|
||||
|
||||
@@ -4,32 +4,31 @@ 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==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.5.0
|
||||
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.1
|
||||
twilio==6.52.0
|
||||
urllib3==1.26.3
|
||||
uWSGI==2.0.19.1
|
||||
validators==0.18.2
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -194,5 +194,12 @@
|
||||
"name": "TRMM Defender Exclusions",
|
||||
"description": "Windows Defender Exclusions for Tactical RMM",
|
||||
"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)]
|
||||
@@ -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
|
||||
|
||||
@@ -33,10 +33,6 @@ app.conf.beat_schedule = {
|
||||
"task": "agents.tasks.auto_self_agent_update_task",
|
||||
"schedule": crontab(minute=35, hour="*"),
|
||||
},
|
||||
"remove-salt": {
|
||||
"task": "agents.tasks.remove_salt_task",
|
||||
"schedule": crontab(minute=14, hour="*/2"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -50,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
|
||||
|
||||
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.3"
|
||||
TRMM_VERSION = "0.4.11"
|
||||
|
||||
# bump this version everytime vue code is changed
|
||||
# to alert user they need to manually refresh their browser
|
||||
APP_VER = "0.0.109"
|
||||
APP_VER = "0.0.113"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.4.1"
|
||||
LATEST_AGENT_VER = "1.4.6"
|
||||
|
||||
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 = "8"
|
||||
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,7 +19,9 @@ 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:
|
||||
@@ -44,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
|
||||
@@ -129,6 +133,10 @@ def bulk_install_updates_task(pks: List[int]) -> None:
|
||||
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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@ def install_updates(request, pk):
|
||||
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,7 +23,7 @@ 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
|
||||
@@ -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
|
||||
|
||||
19
backup.sh
19
backup.sh
@@ -1,6 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="7"
|
||||
#####################################################
|
||||
|
||||
POSTGRES_USER="changeme"
|
||||
POSTGRES_PW="hunter2"
|
||||
|
||||
#####################################################
|
||||
|
||||
SCRIPT_VERSION="9"
|
||||
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
|
||||
@@ -53,7 +53,6 @@ fi
|
||||
printf >&2 "${GREEN}Running postgres vacuum${NC}\n"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction"
|
||||
sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage"
|
||||
|
||||
dt_now=$(date '+%Y_%m_%d__%H_%M_%S')
|
||||
tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX)
|
||||
|
||||
@@ -40,7 +40,7 @@ COPY api/tacticalrmm ${TACTICAL_TMP_DIR}/api
|
||||
COPY scripts ${TACTICAL_TMP_DIR}/scripts
|
||||
|
||||
# copy go install from build stage
|
||||
COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go
|
||||
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
|
||||
COPY --from=CREATE_VENV_STAGE ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
|
||||
# install deps
|
||||
|
||||
69
docs/docs/alerting.md
Normal file
69
docs/docs/alerting.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Alerting Overview
|
||||
|
||||
## Notification Types
|
||||
|
||||
* *Email Alerts* - Sends email
|
||||
* *SMS Alerts* - Sends text message
|
||||
* *Dashboard Alerts* - Adds a notification in the dashboard alert icon
|
||||
|
||||
|
||||
## Alert Severities
|
||||
|
||||
* Informational
|
||||
* Warning
|
||||
* Error
|
||||
|
||||
#### Agents
|
||||
Agent offline alerts always have an error severity.
|
||||
|
||||
#### Checks
|
||||
Checks can be configured to create alerts with different severities
|
||||
|
||||
* Memory and Cpuload checks can be configured with a warning and error threshold. To disable one of them put in a 0.
|
||||
* Script checks allow for information and warning return codes. Everything else, besides a 0 will result in an error severity.
|
||||
* Event Log, service, and ping checks require you to set the severity to information, warning, or error.
|
||||
|
||||
#### Automated Tasks
|
||||
For automated tasks, you set the what the alert severity should be directly on the task.
|
||||
|
||||
|
||||
## Configure Alert Templates
|
||||
Alert template allow you to setup alerting and notifications on many agents at once. Alert templates can be applied to Sites, Client, Automation Policies, and in the Global Settings.
|
||||
|
||||
To create an alert template, go to Settings > Alerts Manager. Then click New
|
||||
|
||||
In the form, give the alert template a name and make sure it is enabled.
|
||||
|
||||
Optionally setup any of the below settings:
|
||||
* *Failure Action* - Runs the selected script once on any agent. This is useful for running one-time tasks like sending an http request to an external system to create a ticket.
|
||||
* *Failure action args* - Optionally pass in arguments to the failure script.
|
||||
* *Failure action timeout* - Sets the timeout for the script.
|
||||
* *Resolved action* - Runs the selected script once on any agent if the alert is resolved. This is useful for running onetime tasks like sending an http request to an external system to close the ticket that was created.
|
||||
* *Resolved action args* - Optionally pass in arguments to the resolved script.
|
||||
* *Resolved action timeout* - Sets the timeout for the script.
|
||||
* *Email Recipients* - Overrides the default email recipients in Global Settings.
|
||||
* *From Email* - Overrides the From email address in Global Settings.
|
||||
* *SMS Recipients* - Overrides the SMS recipients in Global Settings.
|
||||
* *Include desktops* - Will apply to desktops
|
||||
#### agent/check/task settings
|
||||
* *Email on resolved* - Sends a email when the alert clears
|
||||
* *Text on resolved* - Sends a text when the alert clears
|
||||
* *Always email* - This enables the email notification setting on the agent/check/task
|
||||
* *Always sms* - This enables the text notification setting on the agent/check/task
|
||||
* *Always dashboard alert* - This enables the dashboard alert notification setting on the agent/check/task
|
||||
* *Periodic notification* - This sets up a periodic notification on for the agent/check/task alert
|
||||
* *Alert on severity* - When configured, will only send a notification through the corresponding channel if the alert is of the specified severity
|
||||
|
||||
## Applying Alert Templates
|
||||
|
||||
Alerts are applied in the following over. The agent picks the closest matching alert template.
|
||||
|
||||
* Right-click on any Client or Site and go to Assign Alert Template
|
||||
* In Automation Manager, click on Assign Alert Template for the policy you want to apply it to
|
||||
* In Global Settings, select the default alert template
|
||||
|
||||
1. Policy w/ Alert Template applied to Site
|
||||
2. Site
|
||||
3. Policy w/ Alert Template applied to Client
|
||||
4. Client
|
||||
5. Default Alert Template
|
||||
12
go.mod
12
go.mod
@@ -1,16 +1,12 @@
|
||||
module github.com/wh1te909/tacticalrmm
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.4.0
|
||||
github.com/go-resty/resty/v2 v2.5.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.4
|
||||
github.com/wh1te909/rmmagent v1.4.6
|
||||
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
|
||||
)
|
||||
|
||||
65
go.sum
65
go.sum
@@ -5,14 +5,13 @@ 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-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/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-resty/resty/v2 v2.5.0 h1:WFb5bD49/85PO7WgAjZ+/TJQ+Ty1XOcWEfD1zIFCM1c=
|
||||
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -24,7 +23,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/gonutz/w32 v1.0.1-0.20201105145118-e88c649a9470/go.mod h1:Rc/YP5K9gv0FW4p6X9qL3E7Y56lfMflEol1fLElfMW4=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iamacarpet/go-win64api v0.0.0-20200715182619-8cbc936e1a5a/go.mod h1:oGJx9dz0Ny7HC7U55RZ0Smd6N9p3hXP/+hOFtuYrAxM=
|
||||
@@ -32,12 +30,9 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
|
||||
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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,47 +63,29 @@ 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/shirou/gopsutil/v3 v3.20.12/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
|
||||
github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
|
||||
github.com/shirou/gopsutil/v3 v3.21.1/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=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc=
|
||||
github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4=
|
||||
github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8=
|
||||
github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA=
|
||||
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.6 h1:6cHJQRGe0YCcPJwggPU7X9tlF6Cxn41OX4Vt4YADt0Y=
|
||||
github.com/wh1te909/rmmagent v1.4.6/go.mod h1:qh346DIU177vsveCjMLjdrsMVvt0hFYQHC4uRYL7RLU=
|
||||
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,12 +94,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-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
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/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -133,6 +108,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 +117,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=
|
||||
@@ -152,7 +128,6 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omN
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -162,13 +137,13 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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=
|
||||
|
||||
14
install.sh
14
install.sh
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="35"
|
||||
SCRIPT_VERSION="39"
|
||||
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.16.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.16.linux-amd64.tar.gz -C ${go_tmp}
|
||||
|
||||
sudo mv ${go_tmp}/go /usr/local/rmmgo/
|
||||
rm -rf ${go_tmp}
|
||||
@@ -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
|
||||
|
||||
4
main.go
4
main.go
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
// env CGO_ENABLED=0 go build -v -a -tags netgo -installsuffix netgo -ldflags "-s -w" -o nats-api
|
||||
// env CGO_ENABLED=0 go build -v -a -ldflags "-s -w" -o nats-api
|
||||
|
||||
import (
|
||||
"flag"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/wh1te909/tacticalrmm/natsapi"
|
||||
)
|
||||
|
||||
var version = "1.0.3"
|
||||
var version = "1.0.8"
|
||||
|
||||
func main() {
|
||||
ver := flag.Bool("version", false, "Prints version")
|
||||
|
||||
@@ -161,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user