Compare commits
	
		
			132 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6278240526 | ||
|  | 8c2dc5f57d | ||
|  | 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 | ||
|  | 01ee524049 | ||
|  | af9cb65338 | ||
|  | 8aa11c580b | ||
|  | ada627f444 | ||
|  | a7b6d338c3 | ||
|  | 9f00538b97 | ||
|  | a085015282 | ||
|  | 0b9c220fbb | ||
|  | 0e3d04873d | ||
|  | b7578d939f | ||
|  | b5c28de03f | ||
|  | e17d25c156 | ||
|  | c25dc1b99c | ||
|  | a493a574bd | ||
|  | 4284493dce | ||
|  | 25059de8e1 | ||
|  | 1731b05ad0 | ||
|  | e80dc663ac | ||
|  | 39988a4c2f | ||
|  | 415bff303a | ||
|  | a65eb62a54 | ||
|  | 03b2982128 | ||
|  | bff0527857 | ||
|  | f3b7634254 | ||
|  | 6a9593c0b9 | ||
|  | edb785b8e5 | ||
|  | 26d757b50a | ||
|  | 535079ee87 | ||
|  | ac380c29c1 | ||
|  | 3fd212f26c | ||
|  | 04a3abc651 | ||
|  | 6caf85ddd1 | ||
|  | 16e4071508 | ||
|  | 69e7c4324b | ||
|  | a1c4a8cbe5 | 
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -278,15 +278,11 @@ class TestUserAction(TacticalTestCase): | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "editagent"} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "remotebg"} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "takecontrol"} | ||||
|         data = { | ||||
|             "userui": True, | ||||
|             "agent_dblclick_action": "editagent", | ||||
|             "default_agent_tbl_tab": "mixed", | ||||
|         } | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Agent, AgentOutage, RecoveryAction, Note | ||||
| from .models import Agent, RecoveryAction, Note | ||||
|  | ||||
| admin.site.register(Agent) | ||||
| admin.site.register(AgentOutage) | ||||
| admin.site.register(RecoveryAction) | ||||
| admin.site.register(Note) | ||||
|   | ||||
| @@ -3,19 +3,20 @@ import string | ||||
| import os | ||||
| import json | ||||
|  | ||||
| from model_bakery.recipe import Recipe, seq | ||||
| from model_bakery.recipe import Recipe, foreign_key | ||||
| from itertools import cycle | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
|  | ||||
| from .models import Agent | ||||
|  | ||||
|  | ||||
| def generate_agent_id(hostname): | ||||
|     rand = "".join(random.choice(string.ascii_letters) for _ in range(35)) | ||||
|     return f"{rand}-{hostname}" | ||||
|  | ||||
|  | ||||
| site = Recipe("clients.Site") | ||||
|  | ||||
|  | ||||
| def get_wmi_data(): | ||||
|     with open( | ||||
|         os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json") | ||||
| @@ -24,7 +25,8 @@ def get_wmi_data(): | ||||
|  | ||||
|  | ||||
| agent = Recipe( | ||||
|     Agent, | ||||
|     "agents.Agent", | ||||
|     site=foreign_key(site), | ||||
|     hostname="DESKTOP-TEST123", | ||||
|     version="1.3.0", | ||||
|     monitoring_type=cycle(["workstation", "server"]), | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.4 on 2021-01-29 21:11 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0026_auto_20201125_2334'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='overdue_dashboard_alert', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-06 15:34 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0027_agent_overdue_dashboard_alert'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agentoutage', | ||||
|             name='outage_email_sent_time', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='agentoutage', | ||||
|             name='outage_sms_sent_time', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-10 21:56 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0028_auto_20210206_1534'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.DeleteModel( | ||||
|             name='AgentOutage', | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.6 on 2021-02-16 08:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0029_delete_agentoutage'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='offline_time', | ||||
|             field=models.PositiveIntegerField(default=4), | ||||
|         ), | ||||
|     ] | ||||
| @@ -8,8 +8,10 @@ import validators | ||||
| import msgpack | ||||
| import re | ||||
| from collections import Counter | ||||
| from typing import List | ||||
| from typing import List, Union, Any | ||||
| from loguru import logger | ||||
| import asyncio | ||||
|  | ||||
| from packaging import version as pyver | ||||
| from distutils.version import LooseVersion | ||||
| from nats.aio.client import Client as NATS | ||||
| @@ -18,6 +20,7 @@ from nats.aio.errors import ErrTimeout | ||||
| from django.db import models | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from alerts.models import AlertTemplate | ||||
|  | ||||
| from core.models import CoreSettings, TZ_CHOICES | ||||
| from logs.models import BaseAuditModel | ||||
| @@ -50,6 +53,8 @@ class Agent(BaseAuditModel): | ||||
|     mesh_node_id = models.CharField(null=True, blank=True, max_length=255) | ||||
|     overdue_email_alert = models.BooleanField(default=False) | ||||
|     overdue_text_alert = models.BooleanField(default=False) | ||||
|     overdue_dashboard_alert = models.BooleanField(default=False) | ||||
|     offline_time = models.PositiveIntegerField(default=4) | ||||
|     overdue_time = models.PositiveIntegerField(default=30) | ||||
|     check_interval = models.PositiveIntegerField(default=120) | ||||
|     needs_reboot = models.BooleanField(default=False) | ||||
| @@ -75,6 +80,24 @@ class Agent(BaseAuditModel): | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|  | ||||
|         # get old agent if exists | ||||
|         old_agent = type(self).objects.get(pk=self.pk) if self.pk else None | ||||
|         super(BaseAuditModel, self).save(*args, **kwargs) | ||||
|  | ||||
|         # check if new agent has been create | ||||
|         # or check if policy have changed on agent | ||||
|         # or if site has changed on agent and if so generate-policies | ||||
|         if ( | ||||
|             not old_agent | ||||
|             or old_agent | ||||
|             and old_agent.policy != self.policy | ||||
|             or old_agent.site != self.site | ||||
|         ): | ||||
|             self.generate_checks_from_policies() | ||||
|             self.generate_tasks_from_policies() | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.hostname | ||||
|  | ||||
| @@ -127,7 +150,7 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def status(self): | ||||
|         offline = djangotime.now() - djangotime.timedelta(minutes=6) | ||||
|         offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time) | ||||
|         overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time) | ||||
|  | ||||
|         if self.last_seen is not None: | ||||
| @@ -248,6 +271,63 @@ class Agent(BaseAuditModel): | ||||
|         except: | ||||
|             return ["unknown disk"] | ||||
|  | ||||
|     def run_script( | ||||
|         self, | ||||
|         scriptpk: int, | ||||
|         args: List[str] = [], | ||||
|         timeout: int = 120, | ||||
|         full: bool = False, | ||||
|         wait: bool = False, | ||||
|         run_on_any: bool = False, | ||||
|     ) -> Any: | ||||
|  | ||||
|         from scripts.models import Script | ||||
|  | ||||
|         script = Script.objects.get(pk=scriptpk) | ||||
|         data = { | ||||
|             "func": "runscriptfull" if full else "runscript", | ||||
|             "timeout": timeout, | ||||
|             "script_args": args, | ||||
|             "payload": { | ||||
|                 "code": script.code, | ||||
|                 "shell": script.shell, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         running_agent = self | ||||
|         if run_on_any: | ||||
|             nats_ping = {"func": "ping", "timeout": 1} | ||||
|  | ||||
|             # try on self first | ||||
|             r = asyncio.run(self.nats_cmd(nats_ping)) | ||||
|  | ||||
|             if r == "pong": | ||||
|                 running_agent = self | ||||
|             else: | ||||
|                 online = [ | ||||
|                     agent | ||||
|                     for agent in Agent.objects.only( | ||||
|                         "pk", "last_seen", "overdue_time", "offline_time" | ||||
|                     ) | ||||
|                     if agent.status == "online" | ||||
|                 ] | ||||
|  | ||||
|                 for agent in online: | ||||
|                     r = asyncio.run(agent.nats_cmd(nats_ping)) | ||||
|                     if r == "pong": | ||||
|                         running_agent = agent | ||||
|                         break | ||||
|  | ||||
|                 if running_agent.pk == self.pk: | ||||
|                     return "Unable to find an online agent" | ||||
|  | ||||
|         if wait: | ||||
|             return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True)) | ||||
|         else: | ||||
|             asyncio.run(running_agent.nats_cmd(data, wait=False)) | ||||
|  | ||||
|         return "ok" | ||||
|  | ||||
|     # auto approves updates | ||||
|     def approve_updates(self): | ||||
|         patch_policy = self.get_patch_policy() | ||||
| @@ -381,6 +461,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_workstations: | ||||
|                 continue | ||||
|  | ||||
|             # check if template is excluding servers | ||||
|             elif self.monitoring_type == "server" and template.exclude_servers: | ||||
|                 continue | ||||
|             else: | ||||
|                 return template | ||||
|  | ||||
|         # no alert templates found or agent has been excluded | ||||
|         return None | ||||
|  | ||||
|     def generate_checks_from_policies(self): | ||||
|         from automation.models import Policy | ||||
|  | ||||
| @@ -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"), | ||||
|   | ||||
| @@ -34,6 +34,17 @@ class AgentSerializer(serializers.ModelSerializer): | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class AgentOverdueActionSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_dashboard_alert", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     pending_actions = serializers.SerializerMethodField() | ||||
| @@ -44,6 +55,21 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|     logged_username = serializers.SerializerMethodField() | ||||
|     italic = serializers.SerializerMethodField() | ||||
|     policy = serializers.ReadOnlyField(source="policy.id") | ||||
|     alert_template = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_alert_template(self, obj): | ||||
|         alert_template = obj.get_alert_template() | ||||
|  | ||||
|         if not alert_template: | ||||
|             return None | ||||
|         else: | ||||
|             return { | ||||
|                 "name": alert_template.name, | ||||
|                 "always_email": alert_template.agent_always_email, | ||||
|                 "always_text": alert_template.agent_always_text, | ||||
|                 "always_alert": alert_template.agent_always_alert, | ||||
|             } | ||||
|  | ||||
|     def get_pending_actions(self, obj): | ||||
|         return obj.pendingactions.filter(status="pending").count() | ||||
| @@ -54,7 +80,7 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|         else: | ||||
|             agent_tz = self.context["default_tz"] | ||||
|  | ||||
|         return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S") | ||||
|         return obj.last_seen.astimezone(agent_tz).timestamp() | ||||
|  | ||||
|     def get_logged_username(self, obj) -> str: | ||||
|         if obj.logged_in_username == "None" and obj.status == "online": | ||||
| @@ -71,6 +97,7 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "alert_template", | ||||
|             "hostname", | ||||
|             "agent_id", | ||||
|             "site_name", | ||||
| @@ -83,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 | ||||
|  | ||||
| @@ -114,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,114 +3,82 @@ from loguru import logger | ||||
| from time import sleep | ||||
| import random | ||||
| from packaging import version as pyver | ||||
| from typing import List | ||||
| from typing import List, Union | ||||
| import datetime as dt | ||||
|  | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from agents.models import Agent, AgentOutage | ||||
| from agents.models import Agent | ||||
| from core.models import CoreSettings | ||||
| from logs.models import PendingAction | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| def _check_agent_service(pk: int) -> None: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) | ||||
|     if r == "pong": | ||||
|         logger.info( | ||||
|             f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery" | ||||
|         ) | ||||
|         data = {"func": "recover", "payload": {"mode": "tacagent"}} | ||||
|         asyncio.run(agent.nats_cmd(data, wait=False)) | ||||
|  | ||||
|  | ||||
| def _check_in_full(pk: int) -> None: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def check_in_task() -> None: | ||||
|     q = Agent.objects.only("pk", "version") | ||||
|     agents: List[int] = [ | ||||
|         i.pk for i in q if pyver.parse(i.version) == pyver.parse("1.1.12") | ||||
|     ] | ||||
|     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             _check_in_full(pk) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def monitor_agents_task() -> None: | ||||
|     q = Agent.objects.all() | ||||
|     agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"] | ||||
|     for agent in agents: | ||||
|         _check_agent_service(agent) | ||||
|  | ||||
|  | ||||
| def agent_update(pk: int) -> str: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|  | ||||
|     if pyver.parse(agent.version) <= pyver.parse("1.1.11"): | ||||
|         logger.warning( | ||||
|             f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update." | ||||
|         ) | ||||
|         return "not supported" | ||||
|  | ||||
|     # skip if we can't determine the arch | ||||
|     if agent.arch is None: | ||||
|         logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") | ||||
|         logger.warning( | ||||
|             f"Unable to determine arch on {agent.hostname}. Skipping agent update." | ||||
|         ) | ||||
|         return "noarch" | ||||
|  | ||||
|     version = settings.LATEST_AGENT_VER | ||||
|     url = agent.winagent_dl | ||||
|     inno = agent.win_inno_exe | ||||
|     # removed sqlite in 1.4.0 to get rid of cgo dependency | ||||
|     # 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent | ||||
|     if pyver.parse(agent.version) >= pyver.parse("1.3.0"): | ||||
|         version = settings.LATEST_AGENT_VER | ||||
|         url = agent.winagent_dl | ||||
|         inno = agent.win_inno_exe | ||||
|     else: | ||||
|         version = "1.3.0" | ||||
|         inno = ( | ||||
|             "winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe" | ||||
|         ) | ||||
|         url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}" | ||||
|  | ||||
|     if agent.has_nats: | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.1.11"): | ||||
|             if agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists(): | ||||
|                 action = agent.pendingactions.filter( | ||||
|                     action_type="agentupdate", status="pending" | ||||
|                 ).last() | ||||
|                 if pyver.parse(action.details["version"]) < pyver.parse(version): | ||||
|                     action.delete() | ||||
|                 else: | ||||
|                     return "pending" | ||||
|     if agent.pendingactions.filter( | ||||
|         action_type="agentupdate", status="pending" | ||||
|     ).exists(): | ||||
|         agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).delete() | ||||
|  | ||||
|             PendingAction.objects.create( | ||||
|                 agent=agent, | ||||
|                 action_type="agentupdate", | ||||
|                 details={ | ||||
|                     "url": url, | ||||
|                     "version": version, | ||||
|                     "inno": inno, | ||||
|                 }, | ||||
|             ) | ||||
|         else: | ||||
|             nats_data = { | ||||
|                 "func": "agentupdate", | ||||
|                 "payload": { | ||||
|                     "url": url, | ||||
|                     "version": version, | ||||
|                     "inno": inno, | ||||
|                 }, | ||||
|             } | ||||
|             asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|     PendingAction.objects.create( | ||||
|         agent=agent, | ||||
|         action_type="agentupdate", | ||||
|         details={ | ||||
|             "url": url, | ||||
|             "version": version, | ||||
|             "inno": inno, | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|         return "created" | ||||
|  | ||||
|     return "not supported" | ||||
|     nats_data = { | ||||
|         "func": "agentupdate", | ||||
|         "payload": { | ||||
|             "url": url, | ||||
|             "version": version, | ||||
|             "inno": inno, | ||||
|         }, | ||||
|     } | ||||
|     asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|     return "created" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(pks: List[int], version: str) -> None: | ||||
|     q = Agent.objects.filter(pk__in=pks) | ||||
|     agents: List[int] = [ | ||||
|         i.pk for i in q if pyver.parse(i.version) < pyver.parse(version) | ||||
|     ] | ||||
|     chunks = (agents[i : i + 30] for i in range(0, len(agents), 30)) | ||||
| def send_agent_update_task(pks: List[int]) -> None: | ||||
|     chunks = (pks[i : i + 30] for i in range(0, len(pks), 30)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent_update(pk) | ||||
| @@ -140,103 +108,94 @@ def auto_self_agent_update_task() -> None: | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def get_wmi_task(): | ||||
|     agents = Agent.objects.all() | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
|         if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online" | ||||
|     ] | ||||
|     chunks = (online[i : i + 50] for i in range(0, len(online), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             asyncio.run(agent.nats_cmd({"func": "wmi"}, wait=False)) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
| def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     if not alert.email_sent: | ||||
|         sleep(random.randint(1, 15)) | ||||
|         alert.agent.send_outage_email() | ||||
|         alert.email_sent = djangotime.now() | ||||
|         alert.save(update_fields=["email_sent"]) | ||||
|     else: | ||||
|         if alert_interval: | ||||
|             # send an email only if the last email sent is older than alert interval | ||||
|             delta = djangotime.now() - dt.timedelta(days=alert_interval) | ||||
|             if alert.email_sent < delta: | ||||
|                 sleep(random.randint(1, 10)) | ||||
|                 alert.agent.send_outage_email() | ||||
|                 alert.email_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def sync_sysinfo_task(): | ||||
|     agents = Agent.objects.all() | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
|         if pyver.parse(i.version) >= pyver.parse("1.1.3") | ||||
|         and pyver.parse(i.version) <= pyver.parse("1.1.12") | ||||
|         and i.status == "online" | ||||
|     ] | ||||
| def agent_recovery_email_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     chunks = (online[i : i + 50] for i in range(0, len(online), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False)) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outage_email_task(pk): | ||||
|     sleep(random.randint(1, 15)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_outage_email() | ||||
|     outage.outage_email_sent = True | ||||
|     outage.save(update_fields=["outage_email_sent"]) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|     alert.agent.send_recovery_email() | ||||
|     alert.resolved_email_sent = djangotime.now() | ||||
|     alert.save(update_fields=["resolved_email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_recovery_email_task(pk): | ||||
|     sleep(random.randint(1, 15)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_recovery_email() | ||||
|     outage.recovery_email_sent = True | ||||
|     outage.save(update_fields=["recovery_email_sent"]) | ||||
| def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     if not alert.sms_sent: | ||||
|         sleep(random.randint(1, 15)) | ||||
|         alert.agent.send_outage_sms() | ||||
|         alert.sms_sent = djangotime.now() | ||||
|         alert.save(update_fields=["sms_sent"]) | ||||
|     else: | ||||
|         if alert_interval: | ||||
|             # send an sms only if the last sms sent is older than alert interval | ||||
|             delta = djangotime.now() - dt.timedelta(days=alert_interval) | ||||
|             if alert.sms_sent < delta: | ||||
|                 sleep(random.randint(1, 10)) | ||||
|                 alert.agent.send_outage_sms() | ||||
|                 alert.sms_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outage_sms_task(pk): | ||||
| def agent_recovery_sms_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     sleep(random.randint(1, 3)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_outage_sms() | ||||
|     outage.outage_sms_sent = True | ||||
|     outage.save(update_fields=["outage_sms_sent"]) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|     alert.agent.send_recovery_sms() | ||||
|     alert.resolved_sms_sent = djangotime.now() | ||||
|     alert.save(update_fields=["resolved_sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_recovery_sms_task(pk): | ||||
|     sleep(random.randint(1, 3)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_recovery_sms() | ||||
|     outage.recovery_sms_sent = True | ||||
|     outage.save(update_fields=["recovery_sms_sent"]) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outages_task(): | ||||
| def agent_outages_task() -> None: | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert" | ||||
|         "pk", | ||||
|         "last_seen", | ||||
|         "offline_time", | ||||
|         "overdue_time", | ||||
|         "overdue_email_alert", | ||||
|         "overdue_text_alert", | ||||
|         "overdue_dashboard_alert", | ||||
|     ) | ||||
|  | ||||
|     for agent in agents: | ||||
|         if agent.overdue_email_alert or agent.overdue_text_alert: | ||||
|             if agent.status == "overdue": | ||||
|                 outages = AgentOutage.objects.filter(agent=agent) | ||||
|                 if outages and outages.last().is_active: | ||||
|                     continue | ||||
|  | ||||
|                 outage = AgentOutage(agent=agent) | ||||
|                 outage.save() | ||||
|  | ||||
|                 # add a null check history to allow gaps in graph | ||||
|                 for check in agent.agentchecks.all(): | ||||
|                     check.add_check_history(None) | ||||
|  | ||||
|                 if agent.overdue_email_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_email_task.delay(pk=outage.pk) | ||||
|  | ||||
|                 if agent.overdue_text_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_sms_task.delay(pk=outage.pk) | ||||
|         if agent.status == "overdue": | ||||
|             agent.handle_alert() | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -255,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 | ||||
| @@ -300,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.all() | ||||
|     agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] | ||||
|     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             asyncio.run(agent.nats_cmd({"func": "removesalt"}, wait=False)) | ||||
|             sleep(0.1) | ||||
|         sleep(4) | ||||
|   | ||||
| @@ -4,16 +4,19 @@ from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker | ||||
| from itertools import cycle | ||||
| from typing import List | ||||
| from packaging import version as pyver | ||||
|  | ||||
|  | ||||
| from django.test import TestCase, override_settings | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from logs.models import PendingAction | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from .serializers import AgentSerializer | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from .models import Agent | ||||
| from .tasks import auto_self_agent_update_task | ||||
| from winupdate.models import WinUpdatePolicy | ||||
|  | ||||
|  | ||||
| @@ -64,12 +67,34 @@ class TestAgentViews(TacticalTestCase): | ||||
|     @patch("agents.tasks.send_agent_update_task.delay") | ||||
|     def test_update_agents(self, mock_task): | ||||
|         url = "/agents/updateagents/" | ||||
|         data = {"pks": [1, 2, 3, 5, 10], "version": "0.11.1"} | ||||
|         baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version=settings.LATEST_AGENT_VER, | ||||
|             _quantity=15, | ||||
|         ) | ||||
|         baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.3.0", | ||||
|             _quantity=15, | ||||
|         ) | ||||
|  | ||||
|         pks: List[int] = list( | ||||
|             Agent.objects.only("pk", "version").values_list("pk", flat=True) | ||||
|         ) | ||||
|  | ||||
|         data = {"pks": pks} | ||||
|         expected: List[int] = [ | ||||
|             i.pk | ||||
|             for i in Agent.objects.only("pk", "version") | ||||
|             if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|         ] | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         mock_task.assert_called_with(pks=data["pks"], version=data["version"]) | ||||
|         mock_task.assert_called_with(pks=expected) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
| @@ -162,18 +187,44 @@ class TestAgentViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_get_event_log(self, mock_ret): | ||||
|         url = f"/agents/{self.agent.pk}/geteventlog/Application/30/" | ||||
|     def test_get_event_log(self, nats_cmd): | ||||
|         url = f"/agents/{self.agent.pk}/geteventlog/Application/22/" | ||||
|  | ||||
|         with open( | ||||
|             os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json") | ||||
|         ) as f: | ||||
|             mock_ret.return_value = json.load(f) | ||||
|             nats_cmd.return_value = json.load(f) | ||||
|  | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "eventlog", | ||||
|                 "timeout": 30, | ||||
|                 "payload": { | ||||
|                     "logname": "Application", | ||||
|                     "days": str(22), | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=32, | ||||
|         ) | ||||
|  | ||||
|         mock_ret.return_value = "timeout" | ||||
|         url = f"/agents/{self.agent.pk}/geteventlog/Security/6/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "eventlog", | ||||
|                 "timeout": 180, | ||||
|                 "payload": { | ||||
|                     "logname": "Security", | ||||
|                     "days": str(6), | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=182, | ||||
|         ) | ||||
|  | ||||
|         nats_cmd.return_value = "timeout" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
| @@ -308,7 +359,6 @@ class TestAgentViews(TacticalTestCase): | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertIn("rdp", r.json()["cmd"]) | ||||
|         self.assertNotIn("power", r.json()["cmd"]) | ||||
|         self.assertNotIn("ping", r.json()["cmd"]) | ||||
|  | ||||
|         data.update({"ping": 1, "power": 1}) | ||||
|         r = self.client.post(url, data, format="json") | ||||
| @@ -381,6 +431,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|             "site": site.id, | ||||
|             "monitoring_type": "workstation", | ||||
|             "description": "asjdk234andasd", | ||||
|             "offline_time": 4, | ||||
|             "overdue_time": 300, | ||||
|             "check_interval": 60, | ||||
|             "overdue_email_alert": True, | ||||
| @@ -479,42 +530,20 @@ class TestAgentViews(TacticalTestCase): | ||||
|     def test_overdue_action(self): | ||||
|         url = "/agents/overdueaction/" | ||||
|  | ||||
|         payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"} | ||||
|         payload = {"pk": self.agent.pk, "overdue_email_alert": True} | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertTrue(agent.overdue_email_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "email", "action": "disabled"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertFalse(agent.overdue_email_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "enabled"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertTrue(agent.overdue_text_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "disabled"}) | ||||
|         payload = {"pk": self.agent.pk, "overdue_text_alert": False} | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertFalse(agent.overdue_text_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "email", "action": "523423"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "asdasd3434asdasd"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_list_agents_no_detail(self): | ||||
| @@ -676,6 +705,7 @@ class TestAgentViews(TacticalTestCase): | ||||
| class TestAgentViewsNew(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_agent_counts(self): | ||||
|         url = "/agents/agent_counts/" | ||||
| @@ -686,15 +716,12 @@ class TestAgentViewsNew(TacticalTestCase): | ||||
|             monitoring_type=cycle(["server", "workstation"]), | ||||
|             _quantity=6, | ||||
|         ) | ||||
|         agents = baker.make_recipe( | ||||
|         baker.make_recipe( | ||||
|             "agents.overdue_agent", | ||||
|             monitoring_type=cycle(["server", "workstation"]), | ||||
|             _quantity=6, | ||||
|         ) | ||||
|  | ||||
|         # make an AgentOutage for every overdue agent | ||||
|         baker.make("agents.AgentOutage", agent=cycle(agents), _quantity=6) | ||||
|  | ||||
|         # returned data should be this | ||||
|         data = { | ||||
|             "total_server_count": 6, | ||||
| @@ -758,41 +785,55 @@ 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) | ||||
|         self.assertEqual(r, "created") | ||||
|         action = PendingAction.objects.get(agent__pk=agent64_111.pk) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") | ||||
|         self.assertEqual(action.details["url"], settings.DL_64) | ||||
|         self.assertEqual( | ||||
|             action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe" | ||||
|         ) | ||||
|         self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER) | ||||
|  | ||||
|         agent64 = baker.make_recipe( | ||||
|         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_1112.pk) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") | ||||
|         self.assertEqual( | ||||
|             action.details["url"], | ||||
|             "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe", | ||||
|         ) | ||||
|         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", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.3.0", | ||||
|         ) | ||||
|         nats_cmd.return_value = "ok" | ||||
|         r = agent_update(agent64.pk) | ||||
|         r = agent_update(agent_64_130.pk) | ||||
|         self.assertEqual(r, "created") | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
| @@ -805,108 +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") | ||||
|  | ||||
|     """ @patch("agents.models.Agent.salt_api_async") | ||||
|     @patch("agents.tasks.agent_update") | ||||
|     @patch("agents.tasks.sleep", return_value=None) | ||||
|     def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): | ||||
|         # test 64bit golang agent | ||||
|         self.agent64 = baker.make_recipe( | ||||
|     def test_auto_self_agent_update_task(self, mock_sleep, agent_update): | ||||
|         baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.0.0", | ||||
|             version=settings.LATEST_AGENT_VER, | ||||
|             _quantity=23, | ||||
|         ) | ||||
|         salt_api_async.return_value = True | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_called_with( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe", | ||||
|                 "url": settings.DL_64, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.agent64.delete() | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test 32bit golang agent | ||||
|         self.agent32 = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 7 Professional, 32 bit (build 7601.24544)", | ||||
|             version="1.0.0", | ||||
|         ) | ||||
|         salt_api_async.return_value = True | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_called_with( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe", | ||||
|                 "url": settings.DL_32, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.agent32.delete() | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test agent that has a null os field | ||||
|         self.agentNone = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system=None, | ||||
|             version="1.0.0", | ||||
|         ) | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_not_called() | ||||
|         self.agentNone.delete() | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test auto update disabled in global settings | ||||
|         self.agent64 = baker.make_recipe( | ||||
|         baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.0.0", | ||||
|             version="1.3.0", | ||||
|             _quantity=33, | ||||
|         ) | ||||
|  | ||||
|         self.coresettings.agent_auto_update = False | ||||
|         self.coresettings.save(update_fields=["agent_auto_update"]) | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_not_called() | ||||
|  | ||||
|         # reset core settings | ||||
|         self.agent64.delete() | ||||
|         salt_api_async.reset_mock() | ||||
|         r = auto_self_agent_update_task.s().apply() | ||||
|         self.assertEqual(agent_update.call_count, 0) | ||||
|  | ||||
|         self.coresettings.agent_auto_update = True | ||||
|         self.coresettings.save(update_fields=["agent_auto_update"]) | ||||
|  | ||||
|         # test 64bit python agent | ||||
|         self.agent64py = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="0.11.1", | ||||
|         ) | ||||
|         salt_api_async.return_value = True | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_called_with( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": "winagent-v0.11.2.exe", | ||||
|                 "url": OLD_64_PY_AGENT, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.agent64py.delete() | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test 32bit python agent | ||||
|         self.agent32py = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 7 Professional, 32 bit (build 7601.24544)", | ||||
|             version="0.11.1", | ||||
|         ) | ||||
|         salt_api_async.return_value = True | ||||
|         ret = auto_self_agent_update_task.s().apply() | ||||
|         salt_api_async.assert_called_with( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": "winagent-v0.11.2-x86.exe", | ||||
|                 "url": OLD_32_PY_AGENT, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") """ | ||||
|         r = auto_self_agent_update_task.s().apply() | ||||
|         self.assertEqual(agent_update.call_count, 33) | ||||
|   | ||||
| @@ -18,7 +18,7 @@ from rest_framework.views import APIView | ||||
| from rest_framework.response import Response | ||||
| from rest_framework import status, generics | ||||
|  | ||||
| from .models import Agent, AgentOutage, RecoveryAction, Note | ||||
| from .models import Agent, RecoveryAction, Note | ||||
| from core.models import CoreSettings | ||||
| from scripts.models import Script | ||||
| from logs.models import AuditLog, PendingAction | ||||
| @@ -30,6 +30,7 @@ from .serializers import ( | ||||
|     AgentEditSerializer, | ||||
|     NoteSerializer, | ||||
|     NotesSerializer, | ||||
|     AgentOverdueActionSerializer, | ||||
| ) | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| @@ -58,9 +59,13 @@ def get_agent_versions(request): | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def update_agents(request): | ||||
|     pks = request.data["pks"] | ||||
|     version = request.data["version"] | ||||
|     send_agent_update_task.delay(pks=pks, version=version) | ||||
|     q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version") | ||||
|     pks: List[int] = [ | ||||
|         i.pk | ||||
|         for i in q | ||||
|         if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|     ] | ||||
|     send_agent_update_task.delay(pks=pks) | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| @@ -92,22 +97,17 @@ def uninstall(request): | ||||
| def edit_agent(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["id"]) | ||||
|  | ||||
|     old_site = agent.site.pk | ||||
|     a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|     a_serializer.is_valid(raise_exception=True) | ||||
|     a_serializer.save() | ||||
|  | ||||
|     policy = agent.winupdatepolicy.get() | ||||
|     p_serializer = WinUpdatePolicySerializer( | ||||
|         instance=policy, data=request.data["winupdatepolicy"][0] | ||||
|     ) | ||||
|     p_serializer.is_valid(raise_exception=True) | ||||
|     p_serializer.save() | ||||
|  | ||||
|     # check if site changed and initiate generating correct policies | ||||
|     if old_site != request.data["site"]: | ||||
|         agent.generate_checks_from_policies() | ||||
|         agent.generate_tasks_from_policies() | ||||
|     if "winupdatepolicy" in request.data.keys(): | ||||
|         policy = agent.winupdatepolicy.get() | ||||
|         p_serializer = WinUpdatePolicySerializer( | ||||
|             instance=policy, data=request.data["winupdatepolicy"][0] | ||||
|         ) | ||||
|         p_serializer.is_valid(raise_exception=True) | ||||
|         p_serializer.save() | ||||
|  | ||||
|     return Response("ok") | ||||
|  | ||||
| @@ -183,15 +183,16 @@ def get_event_log(request, pk, logtype, days): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|     timeout = 180 if logtype == "Security" else 30 | ||||
|     data = { | ||||
|         "func": "eventlog", | ||||
|         "timeout": 30, | ||||
|         "timeout": timeout, | ||||
|         "payload": { | ||||
|             "logname": logtype, | ||||
|             "days": str(days), | ||||
|         }, | ||||
|     } | ||||
|     r = asyncio.run(agent.nats_cmd(data, timeout=32)) | ||||
|     r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2)) | ||||
|     if r == "timeout": | ||||
|         return notify_error("Unable to contact the agent") | ||||
|  | ||||
| @@ -242,6 +243,7 @@ class AgentsTableList(generics.ListAPIView): | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_time", | ||||
|             "offline_time", | ||||
|             "last_seen", | ||||
|             "boot_time", | ||||
|             "logged_in_username", | ||||
| @@ -290,6 +292,7 @@ def by_client(request, clientpk): | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_time", | ||||
|             "offline_time", | ||||
|             "last_seen", | ||||
|             "boot_time", | ||||
|             "logged_in_username", | ||||
| @@ -319,6 +322,7 @@ def by_site(request, sitepk): | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_time", | ||||
|             "offline_time", | ||||
|             "last_seen", | ||||
|             "boot_time", | ||||
|             "logged_in_username", | ||||
| @@ -333,26 +337,12 @@ def by_site(request, sitepk): | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def overdue_action(request): | ||||
|     pk = request.data["pk"] | ||||
|     alert_type = request.data["alertType"] | ||||
|     action = request.data["action"] | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     if alert_type == "email" and action == "enabled": | ||||
|         agent.overdue_email_alert = True | ||||
|         agent.save(update_fields=["overdue_email_alert"]) | ||||
|     elif alert_type == "email" and action == "disabled": | ||||
|         agent.overdue_email_alert = False | ||||
|         agent.save(update_fields=["overdue_email_alert"]) | ||||
|     elif alert_type == "text" and action == "enabled": | ||||
|         agent.overdue_text_alert = True | ||||
|         agent.save(update_fields=["overdue_text_alert"]) | ||||
|     elif alert_type == "text" and action == "disabled": | ||||
|         agent.overdue_text_alert = False | ||||
|         agent.save(update_fields=["overdue_text_alert"]) | ||||
|     else: | ||||
|         return Response( | ||||
|             {"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST | ||||
|         ) | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|     serializer = AgentOverdueActionSerializer( | ||||
|         instance=agent, data=request.data, partial=True | ||||
|     ) | ||||
|     serializer.is_valid(raise_exception=True) | ||||
|     serializer.save() | ||||
|     return Response(agent.hostname) | ||||
|  | ||||
|  | ||||
| @@ -473,7 +463,7 @@ def install_agent(request): | ||||
|             f"GOARCH={goarch}", | ||||
|             go_bin, | ||||
|             "build", | ||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", | ||||
|             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||
|             f"-X 'main.Api={api}'", | ||||
|             f"-X 'main.Client={client_id}'", | ||||
|             f"-X 'main.Site={site_id}'", | ||||
| @@ -571,12 +561,10 @@ def install_agent(request): | ||||
|             "/VERYSILENT", | ||||
|             "/SUPPRESSMSGBOXES", | ||||
|             "&&", | ||||
|             "timeout", | ||||
|             "/t", | ||||
|             "10", | ||||
|             "/nobreak", | ||||
|             ">", | ||||
|             "NUL", | ||||
|             "ping", | ||||
|             "127.0.0.1", | ||||
|             "-n", | ||||
|             "5", | ||||
|             "&&", | ||||
|             r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"', | ||||
|             "-m", | ||||
| @@ -714,19 +702,10 @@ def run_script(request): | ||||
|         script=script.name, | ||||
|     ) | ||||
|  | ||||
|     data = { | ||||
|         "func": "runscript", | ||||
|         "timeout": request.data["timeout"], | ||||
|         "script_args": request.data["args"], | ||||
|         "payload": { | ||||
|             "code": script.code, | ||||
|             "shell": script.shell, | ||||
|         }, | ||||
|     } | ||||
|  | ||||
|     if output == "wait": | ||||
|         r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout)) | ||||
|         r = agent.run_script(scriptpk=script.pk, timeout=req_timeout, wait=True) | ||||
|         return Response(r) | ||||
|  | ||||
|     elif output == "email": | ||||
|         if not pyver.parse(agent.version) >= pyver.parse("1.1.12"): | ||||
|             return notify_error("Requires agent version 1.1.12 or greater") | ||||
| @@ -738,13 +717,12 @@ def run_script(request): | ||||
|             agentpk=agent.pk, | ||||
|             scriptpk=script.pk, | ||||
|             nats_timeout=req_timeout, | ||||
|             nats_data=data, | ||||
|             emails=emails, | ||||
|         ) | ||||
|         return Response(f"{script.name} will now be run on {agent.hostname}") | ||||
|     else: | ||||
|         asyncio.run(agent.nats_cmd(data, wait=False)) | ||||
|         return Response(f"{script.name} will now be run on {agent.hostname}") | ||||
|         agent.run_script(scriptpk=script.pk, timeout=req_timeout) | ||||
|  | ||||
|     return Response(f"{script.name} will now be run on {agent.hostname}") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @@ -825,7 +803,7 @@ def bulk(request): | ||||
|     elif request.data["target"] == "agents": | ||||
|         q = Agent.objects.filter(pk__in=request.data["agentPKs"]) | ||||
|     elif request.data["target"] == "all": | ||||
|         q = Agent.objects.all() | ||||
|         q = Agent.objects.only("pk", "monitoring_type") | ||||
|     else: | ||||
|         return notify_error("Something went wrong") | ||||
|  | ||||
| @@ -865,20 +843,43 @@ def bulk(request): | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def agent_counts(request): | ||||
|  | ||||
|     server_offline_count = len( | ||||
|         [ | ||||
|             agent | ||||
|             for agent in Agent.objects.filter(monitoring_type="server").only( | ||||
|                 "pk", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             if not agent.status == "online" | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|     workstation_offline_count = len( | ||||
|         [ | ||||
|             agent | ||||
|             for agent in Agent.objects.filter(monitoring_type="workstation").only( | ||||
|                 "pk", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             if not agent.status == "online" | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|     return Response( | ||||
|         { | ||||
|             "total_server_count": Agent.objects.filter( | ||||
|                 monitoring_type="server" | ||||
|             ).count(), | ||||
|             "total_server_offline_count": AgentOutage.objects.filter( | ||||
|                 recovery_time=None, agent__monitoring_type="server" | ||||
|             ).count(), | ||||
|             "total_server_offline_count": server_offline_count, | ||||
|             "total_workstation_count": Agent.objects.filter( | ||||
|                 monitoring_type="workstation" | ||||
|             ).count(), | ||||
|             "total_workstation_offline_count": AgentOutage.objects.filter( | ||||
|                 recovery_time=None, agent__monitoring_type="workstation" | ||||
|             ).count(), | ||||
|             "total_workstation_offline_count": workstation_offline_count, | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Alert | ||||
| from .models import Alert, AlertTemplate | ||||
|  | ||||
|  | ||||
| admin.site.register(Alert) | ||||
| admin.site.register(AlertTemplate) | ||||
|   | ||||
| @@ -42,4 +42,4 @@ class Migration(migrations.Migration): | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
| @@ -27,4 +27,4 @@ class Migration(migrations.Migration): | ||||
|                 max_length=100, | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
| @@ -28,4 +28,4 @@ class Migration(migrations.Migration): | ||||
|             name="alert_time", | ||||
|             field=models.DateTimeField(auto_now_add=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
							
								
								
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-12 14:08 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0029_delete_agentoutage'), | ||||
|         ('clients', '0008_auto_20201103_1430'), | ||||
|         ('autotasks', '0017_auto_20210210_1512'), | ||||
|         ('scripts', '0005_auto_20201207_1606'), | ||||
|         ('alerts', '0003_auto_20201021_1815'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_execution_time', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_retcode', | ||||
|             field=models.IntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_run', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_stderr', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_stdout', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_timeout', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='alert_type', | ||||
|             field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='assigned_task', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='email_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='hidden', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_execution_time', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_retcode', | ||||
|             field=models.IntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_run', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_stderr', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_stdout', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_timeout', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_email_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_on', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_sms_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='sms_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='snoozed', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alert', | ||||
|             name='severity', | ||||
|             field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='AlertTemplate', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=100)), | ||||
|                 ('is_active', models.BooleanField(default=True)), | ||||
|                 ('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)), | ||||
|                 ('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)), | ||||
|                 ('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)), | ||||
|                 ('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)), | ||||
|                 ('email_from', models.EmailField(blank=True, max_length=254, null=True)), | ||||
|                 ('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')), | ||||
|                 ('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')), | ||||
|                 ('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')), | ||||
|                 ('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')), | ||||
|                 ('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-12 17:45 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0004_auto_20210212_1408'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='alert', | ||||
|             name='action_timeout', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_timeout', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='action_timeout', | ||||
|             field=models.PositiveIntegerField(default=15), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='resolved_action_timeout', | ||||
|             field=models.PositiveIntegerField(default=15), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										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,19 +44,11 @@ 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") | ||||
|     def test_checkrunner_interval(self): | ||||
|         url = f"/api/v3/{self.agent.agent_id}/checkinterval/" | ||||
|         r = self.client.get(url, 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) | ||||
|         self.assertEqual( | ||||
|             r.json(), | ||||
|             {"agent": self.agent.pk, "check_interval": self.agent.check_interval}, | ||||
|         ) | ||||
|   | ||||
| @@ -2,17 +2,18 @@ from django.urls import path | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("checkin/", views.CheckIn.as_view()), | ||||
|     path("hello/", views.Hello.as_view()), | ||||
|     path("checkrunner/", views.CheckRunner.as_view()), | ||||
|     path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()), | ||||
|     path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()), | ||||
|     path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()), | ||||
|     path("<int:pk>/meshinfo/", views.MeshInfo.as_view()), | ||||
|     path("meshexe/", views.MeshExe.as_view()), | ||||
|     path("sysinfo/", views.SysInfo.as_view()), | ||||
|     path("newagent/", views.NewAgent.as_view()), | ||||
|     path("winupdater/", views.WinUpdater.as_view()), | ||||
|     path("<str:agentid>/winupdater/", views.WinUpdater.as_view()), | ||||
|     path("software/", views.Software.as_view()), | ||||
|     path("installer/", views.Installer.as_view()), | ||||
|     path("checkin/", views.CheckIn.as_view()), | ||||
|     path("syncmesh/", views.SyncMeshNodeID.as_view()), | ||||
|     path("choco/", views.Choco.as_view()), | ||||
|     path("winupdates/", views.WinUpdates.as_view()), | ||||
|     path("superseded/", views.SupersededWinUpdate.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import asyncio | ||||
| import os | ||||
| import requests | ||||
| import time | ||||
| from loguru import logger | ||||
| from packaging import version as pyver | ||||
|  | ||||
| @@ -17,68 +17,58 @@ from rest_framework.authtoken.models import Token | ||||
|  | ||||
| from agents.models import Agent | ||||
| from checks.models import Check | ||||
| from checks.utils import bytes2human | ||||
| from autotasks.models import AutomatedTask | ||||
| from accounts.models import User | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
| from software.models import InstalledSoftware | ||||
| from checks.serializers import CheckRunnerGetSerializerV3 | ||||
| from agents.serializers import WinAgentSerializer | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||
| from winupdate.serializers import ApprovedUpdateSerializer | ||||
| from agents.serializers import WinAgentSerializer | ||||
|  | ||||
| from agents.tasks import ( | ||||
|     agent_recovery_email_task, | ||||
|     agent_recovery_sms_task, | ||||
| ) | ||||
| from checks.utils import bytes2human | ||||
| from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| class CheckIn(APIView): | ||||
|     """ | ||||
|     The agent's checkin endpoint | ||||
|     patch: called every 45 to 110 seconds, handles agent updates and recovery | ||||
|     put: called every 5 to 10 minutes, handles basic system info | ||||
|     post: called once on windows service startup | ||||
|     """ | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         updated = False | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if pyver.parse(request.data["version"]) > pyver.parse( | ||||
|             agent.version | ||||
|         ) or pyver.parse(request.data["version"]) == pyver.parse( | ||||
|             settings.LATEST_AGENT_VER | ||||
|         ): | ||||
|             updated = True | ||||
|         agent.version = request.data["version"] | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["version", "last_seen"]) | ||||
|  | ||||
|         if agent.agentoutages.exists() and agent.agentoutages.last().is_active: | ||||
|             last_outage = agent.agentoutages.last() | ||||
|             last_outage.recovery_time = djangotime.now() | ||||
|             last_outage.save(update_fields=["recovery_time"]) | ||||
|         # change agent update pending status to completed if agent has just updated | ||||
|         if ( | ||||
|             updated | ||||
|             and agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists() | ||||
|         ): | ||||
|             agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).update(status="completed") | ||||
|  | ||||
|             if agent.overdue_email_alert: | ||||
|                 agent_recovery_email_task.delay(pk=last_outage.pk) | ||||
|             if agent.overdue_text_alert: | ||||
|                 agent_recovery_sms_task.delay(pk=last_outage.pk) | ||||
|         # handles any alerting actions | ||||
|         agent.handle_alert(checkin=True) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last() | ||||
|         if recovery is not None: | ||||
|             recovery.last_run = djangotime.now() | ||||
|             recovery.save(update_fields=["last_run"]) | ||||
|             return Response(recovery.send()) | ||||
|  | ||||
|         # handle agent update | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).exists(): | ||||
|             update = agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).last() | ||||
|             update.status = "completed" | ||||
|             update.save(update_fields=["status"]) | ||||
|             return Response(update.details) | ||||
|             handle_agent_recovery_task.delay(pk=recovery.pk) | ||||
|             return Response("ok") | ||||
|  | ||||
|         # get any pending actions | ||||
|         if agent.pendingactions.filter(status="pending").exists(): | ||||
| @@ -89,75 +79,13 @@ class CheckIn(APIView): | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|  | ||||
|         if "disks" in request.data.keys(): | ||||
|         if request.data["func"] == "disks": | ||||
|             disks = request.data["disks"] | ||||
|             new = [] | ||||
|             # python agent | ||||
|             if isinstance(disks, dict): | ||||
|                 for k, v in disks.items(): | ||||
|                     new.append(v) | ||||
|             else: | ||||
|                 # golang agent | ||||
|                 for disk in disks: | ||||
|                     tmp = {} | ||||
|                     for k, v in disk.items(): | ||||
|                         tmp["device"] = disk["device"] | ||||
|                         tmp["fstype"] = disk["fstype"] | ||||
|                         tmp["total"] = bytes2human(disk["total"]) | ||||
|                         tmp["used"] = bytes2human(disk["used"]) | ||||
|                         tmp["free"] = bytes2human(disk["free"]) | ||||
|                         tmp["percent"] = int(disk["percent"]) | ||||
|                     new.append(tmp) | ||||
|  | ||||
|             serializer.save(disks=new) | ||||
|             return Response("ok") | ||||
|  | ||||
|         if "logged_in_username" in request.data.keys(): | ||||
|             if request.data["logged_in_username"] != "None": | ||||
|                 serializer.save(last_logged_in_user=request.data["logged_in_username"]) | ||||
|                 return Response("ok") | ||||
|  | ||||
|         serializer.save() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|  | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save(last_seen=djangotime.now()) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class Hello(APIView): | ||||
|     #### DEPRECATED, for agents <= 1.1.9 #### | ||||
|     """ | ||||
|     The agent's checkin endpoint | ||||
|     patch: called every 30 to 120 seconds | ||||
|     post: called on agent windows service startup | ||||
|     """ | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|  | ||||
|         disks = request.data["disks"] | ||||
|         new = [] | ||||
|         # python agent | ||||
|         if isinstance(disks, dict): | ||||
|             for k, v in disks.items(): | ||||
|                 new.append(v) | ||||
|         else: | ||||
|             # golang agent | ||||
|             for disk in disks: | ||||
|                 tmp = {} | ||||
|                 for k, v in disk.items(): | ||||
|                 for _, _ in disk.items(): | ||||
|                     tmp["device"] = disk["device"] | ||||
|                     tmp["fstype"] = disk["fstype"] | ||||
|                     tmp["total"] = bytes2human(disk["total"]) | ||||
| @@ -166,54 +94,174 @@ class Hello(APIView): | ||||
|                     tmp["percent"] = int(disk["percent"]) | ||||
|                 new.append(tmp) | ||||
|  | ||||
|         if request.data["logged_in_username"] == "None": | ||||
|             serializer.save(last_seen=djangotime.now(), disks=new) | ||||
|         else: | ||||
|             serializer.save( | ||||
|                 last_seen=djangotime.now(), | ||||
|                 disks=new, | ||||
|                 last_logged_in_user=request.data["logged_in_username"], | ||||
|             serializer.is_valid(raise_exception=True) | ||||
|             serializer.save(disks=new) | ||||
|             return Response("ok") | ||||
|  | ||||
|         if request.data["func"] == "loggedonuser": | ||||
|             if request.data["logged_in_username"] != "None": | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save(last_logged_in_user=request.data["logged_in_username"]) | ||||
|                 return Response("ok") | ||||
|  | ||||
|         if request.data["func"] == "software": | ||||
|             raw: SoftwareList = request.data["software"] | ||||
|             if not isinstance(raw, list): | ||||
|                 return notify_error("err") | ||||
|  | ||||
|             sw = filter_software(raw) | ||||
|             if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|                 InstalledSoftware(agent=agent, software=sw).save() | ||||
|             else: | ||||
|                 s = agent.installedsoftware_set.first() | ||||
|                 s.software = sw | ||||
|                 s.save(update_fields=["software"]) | ||||
|  | ||||
|             return Response("ok") | ||||
|  | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("ok") | ||||
|  | ||||
|     # called once during tacticalagent windows service startup | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if not agent.choco_installed: | ||||
|             asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False)) | ||||
|  | ||||
|         time.sleep(0.5) | ||||
|         asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SyncMeshNodeID(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if agent.mesh_node_id != request.data["nodeid"]: | ||||
|             agent.mesh_node_id = request.data["nodeid"] | ||||
|             agent.save(update_fields=["mesh_node_id"]) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class Choco(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent.choco_installed = request.data["installed"] | ||||
|         agent.save(update_fields=["choco_installed"]) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class WinUpdates(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         reboot_policy: str = agent.get_patch_policy().reboot_after_install | ||||
|         reboot = False | ||||
|  | ||||
|         if reboot_policy == "always": | ||||
|             reboot = True | ||||
|  | ||||
|         if request.data["needs_reboot"]: | ||||
|             if reboot_policy == "required": | ||||
|                 reboot = True | ||||
|             elif reboot_policy == "never": | ||||
|                 agent.needs_reboot = True | ||||
|                 agent.save(update_fields=["needs_reboot"]) | ||||
|  | ||||
|         if reboot: | ||||
|             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||
|             logger.info(f"{agent.hostname} is rebooting after updates were installed.") | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         u = agent.winupdates.filter(guid=request.data["guid"]).last() | ||||
|         success: bool = request.data["success"] | ||||
|         if success: | ||||
|             u.result = "success" | ||||
|             u.downloaded = True | ||||
|             u.installed = True | ||||
|             u.date_installed = djangotime.now() | ||||
|             u.save( | ||||
|                 update_fields=[ | ||||
|                     "result", | ||||
|                     "downloaded", | ||||
|                     "installed", | ||||
|                     "date_installed", | ||||
|                 ] | ||||
|             ) | ||||
|         else: | ||||
|             u.result = "failed" | ||||
|             u.save(update_fields=["result"]) | ||||
|  | ||||
|         if agent.agentoutages.exists() and agent.agentoutages.last().is_active: | ||||
|             last_outage = agent.agentoutages.last() | ||||
|             last_outage.recovery_time = djangotime.now() | ||||
|             last_outage.save(update_fields=["recovery_time"]) | ||||
|  | ||||
|             if agent.overdue_email_alert: | ||||
|                 agent_recovery_email_task.delay(pk=last_outage.pk) | ||||
|             if agent.overdue_text_alert: | ||||
|                 agent_recovery_sms_task.delay(pk=last_outage.pk) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last() | ||||
|         if recovery is not None: | ||||
|             recovery.last_run = djangotime.now() | ||||
|             recovery.save(update_fields=["last_run"]) | ||||
|             return Response(recovery.send()) | ||||
|  | ||||
|         # handle agent update | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).exists(): | ||||
|             update = agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).last() | ||||
|             update.status = "completed" | ||||
|             update.save(update_fields=["status"]) | ||||
|             return Response(update.details) | ||||
|  | ||||
|         # get any pending actions | ||||
|         if agent.pendingactions.filter(status="pending").exists(): | ||||
|             agent.handle_pending_actions() | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         updates = request.data["wua_updates"] | ||||
|         for update in updates: | ||||
|             if agent.winupdates.filter(guid=update["guid"]).exists(): | ||||
|                 u = agent.winupdates.filter(guid=update["guid"]).last() | ||||
|                 u.downloaded = update["downloaded"] | ||||
|                 u.installed = update["installed"] | ||||
|                 u.save(update_fields=["downloaded", "installed"]) | ||||
|             else: | ||||
|                 try: | ||||
|                     kb = "KB" + update["kb_article_ids"][0] | ||||
|                 except: | ||||
|                     continue | ||||
|  | ||||
|                 WinUpdate( | ||||
|                     agent=agent, | ||||
|                     guid=update["guid"], | ||||
|                     kb=kb, | ||||
|                     title=update["title"], | ||||
|                     installed=update["installed"], | ||||
|                     downloaded=update["downloaded"], | ||||
|                     description=update["description"], | ||||
|                     severity=update["severity"], | ||||
|                     categories=update["categories"], | ||||
|                     category_ids=update["category_ids"], | ||||
|                     kb_article_ids=update["kb_article_ids"], | ||||
|                     more_info_urls=update["more_info_urls"], | ||||
|                     support_url=update["support_url"], | ||||
|                     revision_number=update["revision_number"], | ||||
|                 ).save() | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|  | ||||
|         # more superseded updates cleanup | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.4.2"): | ||||
|             for u in agent.winupdates.filter( | ||||
|                 date_installed__isnull=True, result="failed" | ||||
|             ).exclude(installed=True): | ||||
|                 u.delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SupersededWinUpdate(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         updates = agent.winupdates.filter(guid=request.data["guid"]) | ||||
|         for u in updates: | ||||
|             u.delete() | ||||
|  | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save(last_seen=djangotime.now()) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| @@ -232,7 +280,7 @@ class CheckRunner(APIView): | ||||
|         ret = { | ||||
|             "agent": agent.pk, | ||||
|             "check_interval": agent.check_interval, | ||||
|             "checks": CheckRunnerGetSerializerV3(checks, many=True).data, | ||||
|             "checks": CheckRunnerGetSerializer(checks, many=True).data, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
| @@ -245,6 +293,15 @@ class CheckRunner(APIView): | ||||
|         return Response(status) | ||||
|  | ||||
|  | ||||
| class CheckRunnerInterval(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         return Response({"agent": agent.pk, "check_interval": agent.check_interval}) | ||||
|  | ||||
|  | ||||
| class TaskRunner(APIView): | ||||
|     """ | ||||
|     For the windows golang agent | ||||
| @@ -271,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, | ||||
| @@ -283,74 +342,6 @@ class TaskRunner(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class WinUpdater(APIView): | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent.delete_superseded_updates() | ||||
|         patches = agent.winupdates.filter(action="approve").exclude(installed=True) | ||||
|         return Response(ApprovedUpdateSerializer(patches, many=True).data) | ||||
|  | ||||
|     # agent sends patch results as it's installing them | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         kb = request.data["kb"] | ||||
|         results = request.data["results"] | ||||
|         update = agent.winupdates.get(kb=kb) | ||||
|  | ||||
|         if results == "error" or results == "failed": | ||||
|             update.result = results | ||||
|             update.save(update_fields=["result"]) | ||||
|         elif results == "success": | ||||
|             update.result = "success" | ||||
|             update.downloaded = True | ||||
|             update.installed = True | ||||
|             update.date_installed = djangotime.now() | ||||
|             update.save( | ||||
|                 update_fields=[ | ||||
|                     "result", | ||||
|                     "downloaded", | ||||
|                     "installed", | ||||
|                     "date_installed", | ||||
|                 ] | ||||
|             ) | ||||
|         elif results == "alreadyinstalled": | ||||
|             update.result = "success" | ||||
|             update.downloaded = True | ||||
|             update.installed = True | ||||
|             update.save(update_fields=["result", "downloaded", "installed"]) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     # agent calls this after it's finished installing all patches | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         reboot_policy = agent.get_patch_policy().reboot_after_install | ||||
|         reboot = False | ||||
|  | ||||
|         if reboot_policy == "always": | ||||
|             reboot = True | ||||
|  | ||||
|         if request.data["reboot"]: | ||||
|             if reboot_policy == "required": | ||||
|                 reboot = True | ||||
|             elif reboot_policy == "never": | ||||
|                 agent.needs_reboot = True | ||||
|                 agent.save(update_fields=["needs_reboot"]) | ||||
|  | ||||
|         if reboot: | ||||
|             if agent.has_nats: | ||||
|                 asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||
|                 logger.info( | ||||
|                     f"{agent.hostname} is rebooting after updates were installed." | ||||
|                 ) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SysInfo(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
| @@ -366,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 """ | ||||
|  | ||||
| @@ -453,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,18 +7,23 @@ 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) | ||||
|  | ||||
|     if policy.is_default_server_policy and policy.is_default_workstation_policy: | ||||
|         agents = Agent.objects.all() | ||||
|         agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") | ||||
|     elif policy.is_default_server_policy: | ||||
|         agents = Agent.objects.filter(monitoring_type="server") | ||||
|         agents = Agent.objects.filter(monitoring_type="server").only( | ||||
|             "pk", "monitoring_type" | ||||
|         ) | ||||
|     elif policy.is_default_workstation_policy: | ||||
|         agents = Agent.objects.filter(monitoring_type="workstation") | ||||
|         agents = Agent.objects.filter(monitoring_type="workstation").only( | ||||
|             "pk", "monitoring_type" | ||||
|         ) | ||||
|     else: | ||||
|         agents = policy.related_agents() | ||||
|         agents = policy.related_agents().only("pk") | ||||
|  | ||||
|     for agent in agents: | ||||
|         agent.generate_checks_from_policies() | ||||
| @@ -26,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): | ||||
| @@ -36,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() | ||||
| @@ -45,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, | ||||
| @@ -75,34 +101,33 @@ 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) | ||||
|  | ||||
|     if policy.is_default_server_policy and policy.is_default_workstation_policy: | ||||
|         agents = Agent.objects.all() | ||||
|         agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") | ||||
|     elif policy.is_default_server_policy: | ||||
|         agents = Agent.objects.filter(monitoring_type="server") | ||||
|         agents = Agent.objects.filter(monitoring_type="server").only( | ||||
|             "pk", "monitoring_type" | ||||
|         ) | ||||
|     elif policy.is_default_workstation_policy: | ||||
|         agents = Agent.objects.filter(monitoring_type="workstation") | ||||
|         agents = Agent.objects.filter(monitoring_type="workstation").only( | ||||
|             "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 | ||||
| @@ -121,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): | ||||
|  | ||||
| @@ -413,11 +198,15 @@ class UpdatePatchPolicy(APIView): | ||||
|  | ||||
|         agents = None | ||||
|         if "client" in request.data: | ||||
|             agents = Agent.objects.filter(site__client_id=request.data["client"]) | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||
|                 site__client_id=request.data["client"] | ||||
|             ) | ||||
|         elif "site" in request.data: | ||||
|             agents = Agent.objects.filter(site_id=request.data["site"]) | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||
|                 site_id=request.data["site"] | ||||
|             ) | ||||
|         else: | ||||
|             agents = Agent.objects.all() | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk") | ||||
|  | ||||
|         for agent in agents: | ||||
|             winupdatepolicy = agent.winupdatepolicy.get() | ||||
|   | ||||
| @@ -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.all() | ||||
|         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,59 +639,16 @@ class Check(BaseAuditModel): | ||||
|         # handle status | ||||
|         if self.status == "failing": | ||||
|             self.fail_count += 1 | ||||
|             self.save(update_fields=["status", "fail_count"]) | ||||
|             self.save(update_fields=["status", "fail_count", "alert_severity"]) | ||||
|  | ||||
|         elif self.status == "passing": | ||||
|             if self.fail_count != 0: | ||||
|                 self.fail_count = 0 | ||||
|                 self.save(update_fields=["status", "fail_count"]) | ||||
|             else: | ||||
|                 self.save(update_fields=["status"]) | ||||
|             self.fail_count = 0 | ||||
|             self.save(update_fields=["status", "fail_count", "alert_severity"]) | ||||
|  | ||||
|         if self.fail_count >= self.fails_b4_alert: | ||||
|             if self.email_alert: | ||||
|                 handle_check_email_alert_task.delay(self.pk) | ||||
|             if self.text_alert: | ||||
|                 handle_check_sms_alert_task.delay(self.pk) | ||||
|         self.handle_alert() | ||||
|  | ||||
|         return self.status | ||||
|  | ||||
|     def handle_check(self, data): | ||||
|         if self.check_type != "cpuload" and self.check_type != "memory": | ||||
|  | ||||
|             if data["status"] == "passing" and self.fail_count != 0: | ||||
|                 self.fail_count = 0 | ||||
|                 self.save(update_fields=["fail_count"]) | ||||
|  | ||||
|             elif data["status"] == "failing": | ||||
|                 self.fail_count += 1 | ||||
|                 self.save(update_fields=["fail_count"]) | ||||
|  | ||||
|         else: | ||||
|             self.history.append(data["percent"]) | ||||
|  | ||||
|             if len(self.history) > 15: | ||||
|                 self.history = self.history[-15:] | ||||
|  | ||||
|             self.save(update_fields=["history"]) | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|  | ||||
|             if avg > self.threshold: | ||||
|                 self.status = "failing" | ||||
|                 self.fail_count += 1 | ||||
|                 self.save(update_fields=["status", "fail_count"]) | ||||
|             else: | ||||
|                 self.status = "passing" | ||||
|                 if self.fail_count != 0: | ||||
|                     self.fail_count = 0 | ||||
|                     self.save(update_fields=["status", "fail_count"]) | ||||
|                 else: | ||||
|                     self.save(update_fields=["status"]) | ||||
|  | ||||
|         if self.email_alert and self.fail_count >= self.fails_b4_alert: | ||||
|             handle_check_email_alert_task.delay(self.pk) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(check): | ||||
|         # serializes the check and returns json | ||||
| @@ -514,17 +682,22 @@ class Check(BaseAuditModel): | ||||
|             managed_by_policy=bool(agent), | ||||
|             parent_check=(self.pk if agent else None), | ||||
|             name=self.name, | ||||
|             alert_severity=self.alert_severity, | ||||
|             check_type=self.check_type, | ||||
|             email_alert=self.email_alert, | ||||
|             dashboard_alert=self.dashboard_alert, | ||||
|             text_alert=self.text_alert, | ||||
|             fails_b4_alert=self.fails_b4_alert, | ||||
|             extra_details=self.extra_details, | ||||
|             threshold=self.threshold, | ||||
|             error_threshold=self.error_threshold, | ||||
|             warning_threshold=self.warning_threshold, | ||||
|             disk=self.disk, | ||||
|             ip=self.ip, | ||||
|             script=self.script, | ||||
|             script_args=self.script_args, | ||||
|             timeout=self.timeout, | ||||
|             info_return_codes=self.info_return_codes, | ||||
|             warning_return_codes=self.warning_return_codes, | ||||
|             svc_name=self.svc_name, | ||||
|             svc_display_name=self.svc_display_name, | ||||
|             pass_if_start_pending=self.pass_if_start_pending, | ||||
| @@ -566,19 +739,27 @@ class Check(BaseAuditModel): | ||||
|     def send_email(self): | ||||
|  | ||||
|         CORE = CoreSettings.objects.first() | ||||
|         alert_template = self.agent.get_alert_template() | ||||
|  | ||||
|         body: str = "" | ||||
|         if self.agent: | ||||
|             subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed" | ||||
|         else: | ||||
|             subject = f"{self} Failed" | ||||
|  | ||||
|         if self.check_type == "diskspace": | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.warning_threshold}%" | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             percent_used = [ | ||||
|                 d["percent"] for d in self.agent.disks if d["device"] == self.disk | ||||
|             ][0] | ||||
|             percent_free = 100 - percent_used | ||||
|  | ||||
|             body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%" | ||||
|             body = subject + f" - Free: {percent_free}%, {text}" | ||||
|  | ||||
|         elif self.check_type == "script": | ||||
|  | ||||
| @@ -592,26 +773,29 @@ class Check(BaseAuditModel): | ||||
|             body = self.more_info | ||||
|  | ||||
|         elif self.check_type == "cpuload" or self.check_type == "memory": | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.warning_threshold}%" | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|  | ||||
|             if self.check_type == "cpuload": | ||||
|                 body = ( | ||||
|                     subject | ||||
|                     + f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%" | ||||
|                 ) | ||||
|                 body = subject + f" - Average CPU utilization: {avg}%, {text}" | ||||
|  | ||||
|             elif self.check_type == "memory": | ||||
|                 body = ( | ||||
|                     subject | ||||
|                     + f" - Average memory usage: {avg}%, Threshold: {self.threshold}%" | ||||
|                 ) | ||||
|                 body = subject + f" - Average memory usage: {avg}%, {text}" | ||||
|  | ||||
|         elif self.check_type == "winsvc": | ||||
|  | ||||
|             status = list( | ||||
|                 filter(lambda x: x["name"] == self.svc_name, self.agent.services) | ||||
|             )[0]["status"] | ||||
|             try: | ||||
|                 status = list( | ||||
|                     filter(lambda x: x["name"] == self.svc_name, self.agent.services) | ||||
|                 )[0]["status"] | ||||
|             # catch services that don't exist if policy check | ||||
|             except: | ||||
|                 status = "Unknown" | ||||
|  | ||||
|             body = subject + f" - Status: {status.upper()}" | ||||
|  | ||||
| @@ -637,11 +821,13 @@ class Check(BaseAuditModel): | ||||
|                 except: | ||||
|                     continue | ||||
|  | ||||
|         CORE.send_mail(subject, body) | ||||
|         CORE.send_mail(subject, body, alert_template=alert_template) | ||||
|  | ||||
|     def send_sms(self): | ||||
|  | ||||
|         CORE = CoreSettings.objects.first() | ||||
|         alert_template = self.agent.get_alert_template() | ||||
|         body: str = "" | ||||
|  | ||||
|         if self.agent: | ||||
|             subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed" | ||||
| @@ -649,27 +835,33 @@ class Check(BaseAuditModel): | ||||
|             subject = f"{self} Failed" | ||||
|  | ||||
|         if self.check_type == "diskspace": | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.warning_threshold}%" | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             percent_used = [ | ||||
|                 d["percent"] for d in self.agent.disks if d["device"] == self.disk | ||||
|             ][0] | ||||
|             percent_free = 100 - percent_used | ||||
|             body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%" | ||||
|             body = subject + f" - Free: {percent_free}%, {text}" | ||||
|         elif self.check_type == "script": | ||||
|             body = subject + f" - Return code: {self.retcode}" | ||||
|         elif self.check_type == "ping": | ||||
|             body = subject | ||||
|         elif self.check_type == "cpuload" or self.check_type == "memory": | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.warning_threshold}%" | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|             if self.check_type == "cpuload": | ||||
|                 body = ( | ||||
|                     subject | ||||
|                     + f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%" | ||||
|                 ) | ||||
|                 body = subject + f" - Average CPU utilization: {avg}%, {text}" | ||||
|             elif self.check_type == "memory": | ||||
|                 body = ( | ||||
|                     subject | ||||
|                     + f" - Average memory usage: {avg}%, Threshold: {self.threshold}%" | ||||
|                 ) | ||||
|                 body = subject + f" - Average memory usage: {avg}%, {text}" | ||||
|         elif self.check_type == "winsvc": | ||||
|             status = list( | ||||
|                 filter(lambda x: x["name"] == self.svc_name, self.agent.services) | ||||
| @@ -678,7 +870,21 @@ class Check(BaseAuditModel): | ||||
|         elif self.check_type == "eventlog": | ||||
|             body = subject | ||||
|  | ||||
|         CORE.send_sms(body) | ||||
|         CORE.send_sms(body, alert_template=alert_template) | ||||
|  | ||||
|     def send_resolved_email(self): | ||||
|         CORE = CoreSettings.objects.first() | ||||
|         alert_template = self.agent.get_alert_template() | ||||
|         subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" | ||||
|         body = f"{self} is now back to normal" | ||||
|  | ||||
|         CORE.send_mail(subject, body, alert_template=alert_template) | ||||
|  | ||||
|     def send_resolved_sms(self): | ||||
|         CORE = CoreSettings.objects.first() | ||||
|         alert_template = self.agent.get_alert_template() | ||||
|         subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" | ||||
|         CORE.send_sms(subject, alert_template=alert_template) | ||||
|  | ||||
|  | ||||
| class CheckHistory(models.Model): | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|  | ||||
| @@ -95,101 +156,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializer(serializers.ModelSerializer): | ||||
|     # for the windows agent | ||||
|     # only send data needed for agent to run a check | ||||
|  | ||||
|     assigned_task = serializers.SerializerMethodField() | ||||
|     script = ScriptSerializer(read_only=True) | ||||
|  | ||||
|     def get_assigned_task(self, obj): | ||||
|         if obj.assignedtask.exists(): | ||||
|             # this will not break agents on version 0.10.2 or lower | ||||
|             # newer agents once released will properly handle multiple tasks assigned to a check | ||||
|             task = obj.assignedtask.first() | ||||
|             return AssignedTaskCheckRunnerField(task).data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Check | ||||
|         exclude = [ | ||||
|             "policy", | ||||
|             "managed_by_policy", | ||||
|             "overriden_by_policy", | ||||
|             "parent_check", | ||||
|             "name", | ||||
|             "more_info", | ||||
|             "last_run", | ||||
|             "email_alert", | ||||
|             "text_alert", | ||||
|             "fails_b4_alert", | ||||
|             "fail_count", | ||||
|             "email_sent", | ||||
|             "text_sent", | ||||
|             "outage_history", | ||||
|             "extra_details", | ||||
|             "stdout", | ||||
|             "stderr", | ||||
|             "retcode", | ||||
|             "execution_time", | ||||
|             "svc_display_name", | ||||
|             "svc_policy_mode", | ||||
|             "created_by", | ||||
|             "created_time", | ||||
|             "modified_by", | ||||
|             "modified_time", | ||||
|             "history", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializerV2(serializers.ModelSerializer): | ||||
|     # for the windows __python__ agent | ||||
|     # only send data needed for agent to run a check | ||||
|  | ||||
|     assigned_tasks = serializers.SerializerMethodField() | ||||
|     script = ScriptSerializer(read_only=True) | ||||
|  | ||||
|     def get_assigned_tasks(self, obj): | ||||
|         if obj.assignedtask.exists(): | ||||
|             tasks = obj.assignedtask.all() | ||||
|             return AssignedTaskCheckRunnerField(tasks, many=True).data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Check | ||||
|         exclude = [ | ||||
|             "policy", | ||||
|             "managed_by_policy", | ||||
|             "overriden_by_policy", | ||||
|             "parent_check", | ||||
|             "name", | ||||
|             "more_info", | ||||
|             "last_run", | ||||
|             "email_alert", | ||||
|             "text_alert", | ||||
|             "fails_b4_alert", | ||||
|             "fail_count", | ||||
|             "email_sent", | ||||
|             "text_sent", | ||||
|             "outage_history", | ||||
|             "extra_details", | ||||
|             "stdout", | ||||
|             "stderr", | ||||
|             "retcode", | ||||
|             "execution_time", | ||||
|             "svc_display_name", | ||||
|             "svc_policy_mode", | ||||
|             "created_by", | ||||
|             "created_time", | ||||
|             "modified_by", | ||||
|             "modified_time", | ||||
|             "history", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializerV3(serializers.ModelSerializer): | ||||
|     # for the windows __golang__ agent | ||||
|     # only send data needed for agent to run a check | ||||
|     # the difference here is in the script serializer | ||||
|     # script checks no longer rely on salt and are executed directly by the go agent | ||||
|  | ||||
|     assigned_tasks = serializers.SerializerMethodField() | ||||
|     script = ScriptCheckSerializer(read_only=True) | ||||
|  | ||||
| @@ -212,8 +179,6 @@ class CheckRunnerGetSerializerV3(serializers.ModelSerializer): | ||||
|             "text_alert", | ||||
|             "fails_b4_alert", | ||||
|             "fail_count", | ||||
|             "email_sent", | ||||
|             "text_sent", | ||||
|             "outage_history", | ||||
|             "extra_details", | ||||
|             "stdout", | ||||
|   | ||||
| @@ -1,57 +1,90 @@ | ||||
| import datetime as dt | ||||
| import random | ||||
| from time import sleep | ||||
| from typing import Union | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_check_email_alert_task(pk): | ||||
|     from .models import Check | ||||
| def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     check = Check.objects.get(pk=pk) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     if not check.agent.maintenance_mode: | ||||
|         # first time sending email | ||||
|         if not check.email_sent: | ||||
|             sleep(random.randint(1, 10)) | ||||
|             check.send_email() | ||||
|             check.email_sent = djangotime.now() | ||||
|             check.save(update_fields=["email_sent"]) | ||||
|         else: | ||||
|             # send an email only if the last email sent is older than 24 hours | ||||
|             delta = djangotime.now() - dt.timedelta(hours=24) | ||||
|             if check.email_sent < delta: | ||||
|     # first time sending email | ||||
|     if not alert.email_sent: | ||||
|         sleep(random.randint(1, 10)) | ||||
|         alert.assigned_check.send_email() | ||||
|         alert.email_sent = djangotime.now() | ||||
|         alert.save(update_fields=["email_sent"]) | ||||
|     else: | ||||
|         if alert_interval: | ||||
|             # send an email only if the last email sent is older than alert interval | ||||
|             delta = djangotime.now() - dt.timedelta(days=alert_interval) | ||||
|             if alert.email_sent < delta: | ||||
|                 sleep(random.randint(1, 10)) | ||||
|                 check.send_email() | ||||
|                 check.email_sent = djangotime.now() | ||||
|                 check.save(update_fields=["email_sent"]) | ||||
|                 alert.assigned_check.send_email() | ||||
|                 alert.email_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_check_sms_alert_task(pk): | ||||
|     from .models import Check | ||||
| def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     check = Check.objects.get(pk=pk) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     if not check.agent.maintenance_mode: | ||||
|         # first time sending text | ||||
|         if not check.text_sent: | ||||
|             sleep(random.randint(1, 3)) | ||||
|             check.send_sms() | ||||
|             check.text_sent = djangotime.now() | ||||
|             check.save(update_fields=["text_sent"]) | ||||
|         else: | ||||
|     # first time sending text | ||||
|     if not alert.sms_sent: | ||||
|         sleep(random.randint(1, 3)) | ||||
|         alert.assigned_check.send_sms() | ||||
|         alert.sms_sent = djangotime.now() | ||||
|         alert.save(update_fields=["sms_sent"]) | ||||
|     else: | ||||
|         if alert_interval: | ||||
|             # send a text only if the last text sent is older than 24 hours | ||||
|             delta = djangotime.now() - dt.timedelta(hours=24) | ||||
|             if check.text_sent < delta: | ||||
|             delta = djangotime.now() - dt.timedelta(days=alert_interval) | ||||
|             if alert.sms_sent < delta: | ||||
|                 sleep(random.randint(1, 3)) | ||||
|                 check.send_sms() | ||||
|                 check.text_sent = djangotime.now() | ||||
|                 check.save(update_fields=["text_sent"]) | ||||
|                 alert.assigned_check.send_sms() | ||||
|                 alert.sms_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_resolved_check_sms_alert_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     # first time sending text | ||||
|     if not alert.resolved_sms_sent: | ||||
|         sleep(random.randint(1, 3)) | ||||
|         alert.assigned_check.send_resolved_sms() | ||||
|         alert.resolved_sms_sent = djangotime.now() | ||||
|         alert.save(update_fields=["resolved_sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_resolved_check_email_alert_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     # first time sending email | ||||
|     if not alert.resolved_email_sent: | ||||
|         sleep(random.randint(1, 10)) | ||||
|         alert.assigned_check.send_resolved_email() | ||||
|         alert.resolved_email_sent = djangotime.now() | ||||
|         alert.save(update_fields=["resolved_email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|   | ||||
| @@ -2,9 +2,9 @@ from checks.models import CheckHistory | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from .serializers import CheckSerializer | ||||
| from django.utils import timezone as djangotime | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker | ||||
| from itertools import cycle | ||||
|  | ||||
|  | ||||
| class TestCheckViews(TacticalTestCase): | ||||
| @@ -23,7 +23,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data) | ||||
|         self.check_not_authenticated("post", url) | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_add_disk_check(self): | ||||
|         # setup data | ||||
| @@ -36,7 +36,8 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "C:", | ||||
|                 "threshold": 55, | ||||
|                 "error_threshold": 55, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 3, | ||||
|             }, | ||||
|         } | ||||
| @@ -50,7 +51,8 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "C:", | ||||
|                 "threshold": 55, | ||||
|                 "error_threshold": 55, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 3, | ||||
|             }, | ||||
|         } | ||||
| @@ -58,6 +60,38 @@ class TestCheckViews(TacticalTestCase): | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # this should fail because both error and warning threshold are 0 | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "C:", | ||||
|                 "error_threshold": 0, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 3, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # this should fail because both error is greater than warning threshold | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "C:", | ||||
|                 "error_threshold": 50, | ||||
|                 "warning_threshold": 30, | ||||
|                 "fails_b4_alert": 3, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_add_cpuload_check(self): | ||||
|         url = "/checks/checks/" | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
| @@ -65,7 +99,8 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "cpuload", | ||||
|                 "threshold": 66, | ||||
|                 "error_threshold": 66, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
| @@ -73,7 +108,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|         resp = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         payload["threshold"] = 87 | ||||
|         payload["error_threshold"] = 87 | ||||
|         resp = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|         self.assertEqual( | ||||
| @@ -81,6 +116,36 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "A cpuload check for this agent already exists", | ||||
|         ) | ||||
|  | ||||
|         # should fail because both error and warning thresholds are 0 | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "cpuload", | ||||
|                 "error_threshold": 0, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # should fail because error is less than warning | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "cpuload", | ||||
|                 "error_threshold": 10, | ||||
|                 "warning_threshold": 50, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_add_memory_check(self): | ||||
|         url = "/checks/checks/" | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
| @@ -88,7 +153,8 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "memory", | ||||
|                 "threshold": 78, | ||||
|                 "error_threshold": 78, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 1, | ||||
|             }, | ||||
|         } | ||||
| @@ -96,7 +162,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|         resp = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         payload["threshold"] = 55 | ||||
|         payload["error_threshold"] = 55 | ||||
|         resp = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|         self.assertEqual( | ||||
| @@ -104,6 +170,34 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "A memory check for this agent already exists", | ||||
|         ) | ||||
|  | ||||
|         # should fail because both error and warning thresholds are 0 | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "memory", | ||||
|                 "error_threshold": 0, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # should fail because error is less than warning | ||||
|         invalid_payload = { | ||||
|             "pk": agent.pk, | ||||
|             "check": { | ||||
|                 "check_type": "memory", | ||||
|                 "error_threshold": 10, | ||||
|                 "warning_threshold": 50, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|     def test_get_policy_disk_check(self): | ||||
|         # setup data | ||||
|         policy = baker.make("automation.Policy") | ||||
| @@ -129,11 +223,37 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "M:", | ||||
|                 "threshold": 86, | ||||
|                 "error_threshold": 86, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 2, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         # should fail because both error and warning thresholds are 0 | ||||
|         invalid_payload = { | ||||
|             "policy": policy.pk, | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "error_threshold": 0, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, invalid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # should fail because warning is less than error | ||||
|         invalid_payload = { | ||||
|             "policy": policy.pk, | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "error_threshold": 80, | ||||
|                 "warning_threshold": 50, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, valid_payload, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
| @@ -143,7 +263,8 @@ class TestCheckViews(TacticalTestCase): | ||||
|             "check": { | ||||
|                 "check_type": "diskspace", | ||||
|                 "disk": "M:", | ||||
|                 "threshold": 34, | ||||
|                 "error_threshold": 34, | ||||
|                 "warning_threshold": 0, | ||||
|                 "fails_b4_alert": 9, | ||||
|             }, | ||||
|         } | ||||
| @@ -184,6 +305,48 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("patch", url_a) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_run_checks(self, nats_cmd): | ||||
|         agent = baker.make_recipe("agents.agent", version="1.4.1") | ||||
|         agent_old = baker.make_recipe("agents.agent", version="1.0.2") | ||||
|         agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0") | ||||
|  | ||||
|         url = f"/checks/runchecks/{agent_old.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|         url = f"/checks/runchecks/{agent_b4_141.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, wait=False) | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "busy" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}") | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "ok" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}") | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "timeout" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), "Unable to contact the agent") | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_get_check_history(self): | ||||
|         # setup data | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import asyncio | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.db.models import Q | ||||
| @@ -168,8 +169,17 @@ def run_checks(request, pk): | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|     asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) | ||||
|     return Response(agent.hostname) | ||||
|     if pyver.parse(agent.version) >= pyver.parse("1.4.1"): | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15)) | ||||
|         if r == "busy": | ||||
|             return notify_error(f"Checks are already running on {agent.hostname}") | ||||
|         elif r == "ok": | ||||
|             return Response(f"Checks will now be re-run on {agent.hostname}") | ||||
|         else: | ||||
|             return notify_error("Unable to contact the agent") | ||||
|     else: | ||||
|         asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) | ||||
|         return Response(f"Checks will now be re-run on {agent.hostname}") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-12 14:08 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0004_auto_20210212_1408'), | ||||
|         ('clients', '0008_auto_20201103_1430'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='client', | ||||
|             name='alert_template', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='alerts.alerttemplate'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='site', | ||||
|             name='alert_template', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='alerts.alerttemplate'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -23,6 +23,36 @@ class Client(BaseAuditModel): | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     alert_template = models.ForeignKey( | ||||
|         "alerts.AlertTemplate", | ||||
|         related_name="clients", | ||||
|         on_delete=models.SET_NULL, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     def save(self, *args, **kw): | ||||
|         from automation.tasks import generate_agent_checks_by_location_task | ||||
|  | ||||
|         # get old client if exists | ||||
|         old_client = type(self).objects.get(pk=self.pk) if self.pk else None | ||||
|         super(BaseAuditModel, self).save(*args, **kw) | ||||
|  | ||||
|         # check if server polcies have changed and initiate task to reapply policies if so | ||||
|         if old_client and old_client.server_policy != self.server_policy: | ||||
|             generate_agent_checks_by_location_task.delay( | ||||
|                 location={"site__client_id": self.pk}, | ||||
|                 mon_type="server", | ||||
|                 create_tasks=True, | ||||
|             ) | ||||
|  | ||||
|         # check if workstation polcies have changed and initiate task to reapply policies if so | ||||
|         if old_client and old_client.workstation_policy != self.workstation_policy: | ||||
|             generate_agent_checks_by_location_task.delay( | ||||
|                 location={"site__client_id": self.pk}, | ||||
|                 mon_type="workstation", | ||||
|                 create_tasks=True, | ||||
|             ) | ||||
|  | ||||
|     class Meta: | ||||
|         ordering = ("name",) | ||||
| @@ -45,6 +75,7 @@ class Client(BaseAuditModel): | ||||
|                 "overdue_text_alert", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             .filter(site__client=self) | ||||
|             .prefetch_related("agentchecks") | ||||
| @@ -87,6 +118,36 @@ class Site(BaseAuditModel): | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     alert_template = models.ForeignKey( | ||||
|         "alerts.AlertTemplate", | ||||
|         related_name="sites", | ||||
|         on_delete=models.SET_NULL, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     def save(self, *args, **kw): | ||||
|         from automation.tasks import generate_agent_checks_by_location_task | ||||
|  | ||||
|         # get old client if exists | ||||
|         old_site = type(self).objects.get(pk=self.pk) if self.pk else None | ||||
|         super(Site, self).save(*args, **kw) | ||||
|  | ||||
|         # check if server polcies have changed and initiate task to reapply policies if so | ||||
|         if old_site and old_site.server_policy != self.server_policy: | ||||
|             generate_agent_checks_by_location_task.delay( | ||||
|                 location={"site_id": self.pk}, | ||||
|                 mon_type="server", | ||||
|                 create_tasks=True, | ||||
|             ) | ||||
|  | ||||
|         # check if workstation polcies have changed and initiate task to reapply policies if so | ||||
|         if old_site and old_site.workstation_policy != self.workstation_policy: | ||||
|             generate_agent_checks_by_location_task.delay( | ||||
|                 location={"site_id": self.pk}, | ||||
|                 mon_type="workstation", | ||||
|                 create_tasks=True, | ||||
|             ) | ||||
|  | ||||
|     class Meta: | ||||
|         ordering = ("name",) | ||||
| @@ -107,6 +168,7 @@ class Site(BaseAuditModel): | ||||
|                 "overdue_text_alert", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             .filter(site=self) | ||||
|             .prefetch_related("agentchecks") | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class SiteSerializer(ModelSerializer): | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def validate(self, val): | ||||
|         if "|" in val["name"]: | ||||
|         if "name" in val.keys() and "|" in val["name"]: | ||||
|             raise ValidationError("Site name cannot contain the | character") | ||||
|  | ||||
|         if self.context: | ||||
| @@ -36,7 +36,7 @@ class ClientSerializer(ModelSerializer): | ||||
|             if len(self.context["site"]) > 255: | ||||
|                 raise ValidationError("Site name too long") | ||||
|  | ||||
|         if "|" in val["name"]: | ||||
|         if "name" in val.keys() and "|" in val["name"]: | ||||
|             raise ValidationError("Client name cannot contain the | character") | ||||
|  | ||||
|         return val | ||||
|   | ||||
| @@ -61,7 +61,8 @@ class GetAddClients(APIView): | ||||
| class GetUpdateDeleteClient(APIView): | ||||
|     def put(self, request, pk): | ||||
|         client = get_object_or_404(Client, pk=pk) | ||||
|         serializer = ClientSerializer(data=request.data, instance=client) | ||||
|  | ||||
|         serializer = ClientSerializer(data=request.data, instance=client, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|  | ||||
| @@ -106,7 +107,7 @@ class GetUpdateDeleteSite(APIView): | ||||
|     def put(self, request, pk): | ||||
|  | ||||
|         site = get_object_or_404(Site, pk=pk) | ||||
|         serializer = SiteSerializer(instance=site, data=request.data) | ||||
|         serializer = SiteSerializer(instance=site, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|  | ||||
| @@ -223,7 +224,7 @@ class GenerateAgent(APIView): | ||||
|             f"GOARCH={goarch}", | ||||
|             go_bin, | ||||
|             "build", | ||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", | ||||
|             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||
|             f"-X 'main.Api={api}'", | ||||
|             f"-X 'main.Client={d.client.pk}'", | ||||
|             f"-X 'main.Site={d.site.pk}'", | ||||
|   | ||||
| @@ -133,7 +133,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	time.Sleep(10 * time.Second) | ||||
| 	time.Sleep(5 * time.Second) | ||||
|  | ||||
| 	fmt.Println("Installation starting.") | ||||
| 	cmd := exec.Command(tacrmm, cmdArgs...) | ||||
|   | ||||
| @@ -36,7 +36,7 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) { | ||||
|         Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output | ||||
|         Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait | ||||
|         write-host ('Extracting...') | ||||
|         Start-Sleep -s 10 | ||||
|         Start-Sleep -s 5 | ||||
|         Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait | ||||
|         exit 0 | ||||
|     } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class Command(BaseCommand): | ||||
|         # 10-16-2020 changed the type of the agent's 'disks' model field | ||||
|         # from a dict of dicts, to a list of disks in the golang agent | ||||
|         # the following will convert dicts to lists for agent's still on the python agent | ||||
|         agents = Agent.objects.all() | ||||
|         agents = Agent.objects.only("pk", "disks") | ||||
|         for agent in agents: | ||||
|             if agent.disks is not None and isinstance(agent.disks, dict): | ||||
|                 new = [] | ||||
| @@ -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.all() | ||||
|         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) | ||||
|   | ||||
| @@ -140,7 +140,7 @@ def cancel_pending_action(request): | ||||
| def debug_log(request, mode, hostname, order): | ||||
|     log_file = settings.LOG_CONFIG["handlers"][0]["sink"] | ||||
|  | ||||
|     agents = Agent.objects.all() | ||||
|     agents = Agent.objects.prefetch_related("site").only("pk", "hostname") | ||||
|     agent_hostnames = AgentHostnameSerializer(agents, many=True) | ||||
|  | ||||
|     switch_mode = { | ||||
|   | ||||
							
								
								
									
										59
									
								
								api/tacticalrmm/natsapi/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/tacticalrmm/natsapi/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| from model_bakery import baker | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from django.conf import settings | ||||
|  | ||||
|  | ||||
| class TestNatsAPIViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_nats_wmi(self): | ||||
|         url = "/natsapi/wmi/" | ||||
|         baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14) | ||||
|         baker.make_recipe( | ||||
|             "agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3 | ||||
|         ) | ||||
|         baker.make_recipe( | ||||
|             "agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5 | ||||
|         ) | ||||
|         baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7) | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(len(r.json()["agent_ids"]), 17) | ||||
|  | ||||
|     def test_natscheckin_patch(self): | ||||
|         from logs.models import PendingAction | ||||
|  | ||||
|         url = "/natsapi/checkin/" | ||||
|         agent_updated = baker.make_recipe("agents.agent", version="1.3.0") | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent_updated, | ||||
|             action_type="agentupdate", | ||||
|             details={ | ||||
|                 "url": agent_updated.winagent_dl, | ||||
|                 "version": agent_updated.version, | ||||
|                 "inno": agent_updated.win_inno_exe, | ||||
|             }, | ||||
|         ) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent failed to update and still on same version | ||||
|         payload = { | ||||
|             "func": "hello", | ||||
|             "agent_id": agent_updated.agent_id, | ||||
|             "version": "1.3.0", | ||||
|         } | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent successful update | ||||
|         payload["version"] = settings.LATEST_AGENT_VER | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "completed") | ||||
|         action.delete() | ||||
| @@ -7,4 +7,8 @@ urlpatterns = [ | ||||
|     path("syncmesh/", views.SyncMeshNodeID.as_view()), | ||||
|     path("winupdates/", views.NatsWinUpdates.as_view()), | ||||
|     path("choco/", views.NatsChoco.as_view()), | ||||
|     path("wmi/", views.NatsWMI.as_view()), | ||||
|     path("offline/", views.OfflineAgents.as_view()), | ||||
|     path("logcrash/", views.LogCrash.as_view()), | ||||
|     path("superseded/", views.SupersededWinUpdate.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import asyncio | ||||
| import time | ||||
| from django.utils import timezone as djangotime | ||||
| from loguru import logger | ||||
| from packaging import version as pyver | ||||
| from typing import List | ||||
|  | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| @@ -43,20 +45,31 @@ class NatsCheckIn(APIView): | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         updated = False | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if pyver.parse(request.data["version"]) > pyver.parse( | ||||
|             agent.version | ||||
|         ) or pyver.parse(request.data["version"]) == pyver.parse( | ||||
|             settings.LATEST_AGENT_VER | ||||
|         ): | ||||
|             updated = True | ||||
|         agent.version = request.data["version"] | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["version", "last_seen"]) | ||||
|  | ||||
|         if agent.agentoutages.exists() and agent.agentoutages.last().is_active: | ||||
|             last_outage = agent.agentoutages.last() | ||||
|             last_outage.recovery_time = djangotime.now() | ||||
|             last_outage.save(update_fields=["recovery_time"]) | ||||
|         # change agent update pending status to completed if agent has just updated | ||||
|         if ( | ||||
|             updated | ||||
|             and agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists() | ||||
|         ): | ||||
|             agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).update(status="completed") | ||||
|  | ||||
|             if agent.overdue_email_alert: | ||||
|                 agent_recovery_email_task.delay(pk=last_outage.pk) | ||||
|             if agent.overdue_text_alert: | ||||
|                 agent_recovery_sms_task.delay(pk=last_outage.pk) | ||||
|         # handles any alerting actions | ||||
|         agent.handle_alert(checkin=True) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last() | ||||
|         if recovery is not None: | ||||
| @@ -176,6 +189,7 @@ class NatsWinUpdates(APIView): | ||||
|             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||
|             logger.info(f"{agent.hostname} is rebooting after updates were installed.") | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def patch(self, request): | ||||
| @@ -199,6 +213,7 @@ class NatsWinUpdates(APIView): | ||||
|             u.result = "failed" | ||||
|             u.save(update_fields=["result"]) | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
| @@ -233,4 +248,68 @@ class NatsWinUpdates(APIView): | ||||
|                     revision_number=update["revision_number"], | ||||
|                 ).save() | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|  | ||||
|         # more superseded updates cleanup | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.4.2"): | ||||
|             for u in agent.winupdates.filter( | ||||
|                 date_installed__isnull=True, result="failed" | ||||
|             ).exclude(installed=True): | ||||
|                 u.delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SupersededWinUpdate(APIView): | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         updates = agent.winupdates.filter(guid=request.data["guid"]) | ||||
|         for u in updates: | ||||
|             u.delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class NatsWMI(APIView): | ||||
|  | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def get(self, request): | ||||
|         agents = Agent.objects.only( | ||||
|             "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time" | ||||
|         ) | ||||
|         online: List[str] = [ | ||||
|             i.agent_id | ||||
|             for i in agents | ||||
|             if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online" | ||||
|         ] | ||||
|         return Response({"agent_ids": online}) | ||||
|  | ||||
|  | ||||
| class OfflineAgents(APIView): | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def get(self, request): | ||||
|         agents = Agent.objects.only( | ||||
|             "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time" | ||||
|         ) | ||||
|         offline: List[str] = [ | ||||
|             i.agent_id for i in agents if i.has_nats and i.status != "online" | ||||
|         ] | ||||
|         return Response({"agent_ids": offline}) | ||||
|  | ||||
|  | ||||
| class LogCrash(APIView): | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agentid"]) | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["last_seen"]) | ||||
|         return Response("ok") | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| black | ||||
| Werkzeug | ||||
| django-extensions | ||||
| mkdocs | ||||
| mkdocs-material | ||||
| pymdown-extensions | ||||
| @@ -1,36 +1,35 @@ | ||||
| amqp==5.0.2 | ||||
| amqp==5.0.5 | ||||
| asgiref==3.3.1 | ||||
| asyncio-nats-client==0.11.4 | ||||
| billiard==3.6.3.0 | ||||
| celery==5.0.5 | ||||
| certifi==2020.12.5 | ||||
| cffi==1.14.4 | ||||
| cffi==1.14.5 | ||||
| chardet==4.0.0 | ||||
| cryptography==3.3.1 | ||||
| cryptography==3.4.4 | ||||
| decorator==4.4.2 | ||||
| Django==3.1.5 | ||||
| django-cors-headers==3.6.0 | ||||
| Django==3.1.6 | ||||
| django-cors-headers==3.7.0 | ||||
| django-rest-knox==4.1.0 | ||||
| djangorestframework==3.12.2 | ||||
| future==0.18.2 | ||||
| idna==2.10 | ||||
| kombu==5.0.2 | ||||
| loguru==0.5.3 | ||||
| msgpack==1.0.2 | ||||
| packaging==20.8 | ||||
| psycopg2-binary==2.8.6 | ||||
| pycparser==2.20 | ||||
| pycryptodome==3.9.9 | ||||
| pyotp==2.4.1 | ||||
| pycryptodome==3.10.1 | ||||
| pyotp==2.6.0 | ||||
| pyparsing==2.4.7 | ||||
| pytz==2020.5 | ||||
| pytz==2021.1 | ||||
| qrcode==6.1 | ||||
| redis==3.5.3 | ||||
| requests==2.25.1 | ||||
| six==1.15.0 | ||||
| sqlparse==0.4.1 | ||||
| twilio==6.51.0 | ||||
| urllib3==1.26.2 | ||||
| twilio==6.52.0 | ||||
| urllib3==1.26.3 | ||||
| uWSGI==2.0.19.1 | ||||
| validators==0.18.2 | ||||
| vine==5.0.0 | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| from .models import Script | ||||
| from model_bakery.recipe import Recipe | ||||
|  | ||||
| script = Recipe( | ||||
|     Script, | ||||
|     "scripts.Script", | ||||
|     name="Test Script", | ||||
|     description="Test Desc", | ||||
|     shell="cmd", | ||||
|   | ||||
| @@ -193,6 +193,13 @@ | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "TRMM Defender Exclusions", | ||||
|         "description": "Windows Defender Exclusions for Tactical RMM", | ||||
|         "shell": "cmd" | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Display_Message_To_User.ps1", | ||||
|         "submittedBy": "https://github.com/bradhawkins85", | ||||
|         "name": "Display Message To User", | ||||
|         "description": "Displays a popup message to the currently logged on user", | ||||
|         "shell": "powershell" | ||||
|     } | ||||
| ] | ||||
| @@ -1,42 +0,0 @@ | ||||
| # Generated by Django 3.1.4 on 2020-12-10 21:45 | ||||
|  | ||||
| from django.db import migrations | ||||
| from django.conf import settings | ||||
| import os | ||||
| import base64 | ||||
| from pathlib import Path | ||||
|  | ||||
|  | ||||
| def move_scripts_to_db(apps, schema_editor): | ||||
|     print("") | ||||
|     Script = apps.get_model("scripts", "Script") | ||||
|     for script in Script.objects.all(): | ||||
|         if not script.script_type == "builtin": | ||||
|  | ||||
|             if script.filename: | ||||
|                 filepath = f"{settings.SCRIPTS_DIR}/userdefined/{script.filename}" | ||||
|             else: | ||||
|                 print(f"No filename on script found. Skipping") | ||||
|                 continue | ||||
|  | ||||
|             # test if file exists | ||||
|             if os.path.exists(filepath): | ||||
|                 print(f"Found script {script.name}. Importing code.") | ||||
|  | ||||
|                 with open(filepath, "rb") as f: | ||||
|                     script_bytes = f.read().decode("utf-8").encode("ascii", "ignore") | ||||
|                     script.code_base64 = base64.b64encode(script_bytes).decode("ascii") | ||||
|                     script.save(update_fields=["code_base64"]) | ||||
|             else: | ||||
|                 print( | ||||
|                     f"Script file {script.name} was not found on the disk. You will need to edit the script in the UI" | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("scripts", "0005_auto_20201207_1606"), | ||||
|     ] | ||||
|  | ||||
|     operations = [migrations.RunPython(move_scripts_to_db, migrations.RunPython.noop)] | ||||
| @@ -72,6 +72,7 @@ class Script(BaseAuditModel): | ||||
|                     i.name = script["name"] | ||||
|                     i.description = script["description"] | ||||
|                     i.category = "Community" | ||||
|                     i.shell = script["shell"] | ||||
|  | ||||
|                     with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: | ||||
|                         script_bytes = ( | ||||
| @@ -80,7 +81,13 @@ class Script(BaseAuditModel): | ||||
|                         i.code_base64 = base64.b64encode(script_bytes).decode("ascii") | ||||
|  | ||||
|                     i.save( | ||||
|                         update_fields=["name", "description", "category", "code_base64"] | ||||
|                         update_fields=[ | ||||
|                             "name", | ||||
|                             "description", | ||||
|                             "category", | ||||
|                             "code_base64", | ||||
|                             "shell", | ||||
|                         ] | ||||
|                     ) | ||||
|                 else: | ||||
|                     print(f"Adding new community script: {script['name']}") | ||||
|   | ||||
| @@ -195,15 +195,21 @@ class TestScriptViews(TacticalTestCase): | ||||
|             info = json.load(f) | ||||
|  | ||||
|         for script in info: | ||||
|             self.assertTrue( | ||||
|                 os.path.exists(os.path.join(scripts_dir, script["filename"])) | ||||
|             ) | ||||
|             fn: str = script["filename"] | ||||
|             self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn))) | ||||
|             self.assertTrue(script["filename"]) | ||||
|             self.assertTrue(script["name"]) | ||||
|             self.assertTrue(script["description"]) | ||||
|             self.assertTrue(script["shell"]) | ||||
|             self.assertIn(script["shell"], valid_shells) | ||||
|  | ||||
|             if fn.endswith(".ps1"): | ||||
|                 self.assertEqual(script["shell"], "powershell") | ||||
|             elif fn.endswith(".bat"): | ||||
|                 self.assertEqual(script["shell"], "cmd") | ||||
|             elif fn.endswith(".py"): | ||||
|                 self.assertEqual(script["shell"], "python") | ||||
|  | ||||
|     def test_load_community_scripts(self): | ||||
|         with open( | ||||
|             os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from agents.models import Agent | ||||
| class TestServiceViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_default_services(self): | ||||
|         url = "/services/defaultservices/" | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from .models import ChocoLog | ||||
| class TestSoftwareViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_chocos_get(self): | ||||
|         url = "/software/chocos/" | ||||
| @@ -63,6 +64,9 @@ class TestSoftwareViews(TacticalTestCase): | ||||
|  | ||||
|  | ||||
| class TestSoftwareTasks(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_install_program(self, nats_cmd): | ||||
|         from .tasks import install_program | ||||
|   | ||||
| @@ -29,30 +29,10 @@ app.conf.beat_schedule = { | ||||
|         "task": "winupdate.tasks.check_agent_update_schedule_task", | ||||
|         "schedule": crontab(minute=5, hour="*"), | ||||
|     }, | ||||
|     "agents-checkinfull": { | ||||
|         "task": "agents.tasks.check_in_task", | ||||
|         "schedule": crontab(minute="*/24"), | ||||
|     }, | ||||
|     "agent-auto-update": { | ||||
|         "task": "agents.tasks.auto_self_agent_update_task", | ||||
|         "schedule": crontab(minute=35, hour="*"), | ||||
|     }, | ||||
|     "agents-sync": { | ||||
|         "task": "agents.tasks.sync_sysinfo_task", | ||||
|         "schedule": crontab(minute=55, hour="*"), | ||||
|     }, | ||||
|     "get-wmi": { | ||||
|         "task": "agents.tasks.get_wmi_task", | ||||
|         "schedule": crontab(minute="*/18"), | ||||
|     }, | ||||
|     "check-agentservice": { | ||||
|         "task": "agents.tasks.monitor_agents_task", | ||||
|         "schedule": crontab(minute="*/15"), | ||||
|     }, | ||||
|     "remove-salt": { | ||||
|         "task": "agents.tasks.remove_salt_task", | ||||
|         "schedule": crontab(minute=14, hour="*/2"), | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -66,6 +46,8 @@ def setup_periodic_tasks(sender, **kwargs): | ||||
|  | ||||
|     from agents.tasks import agent_outages_task | ||||
|     from core.tasks import core_maintenance_tasks | ||||
|     from alerts.tasks import unsnooze_alerts | ||||
|  | ||||
|     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.0" | ||||
| TRMM_VERSION = "0.4.12" | ||||
|  | ||||
| # bump this version everytime vue code is changed | ||||
| # to alert user they need to manually refresh their browser | ||||
| APP_VER = "0.0.107" | ||||
| APP_VER = "0.0.113" | ||||
|  | ||||
| # https://github.com/wh1te909/rmmagent | ||||
| LATEST_AGENT_VER = "1.3.0" | ||||
| LATEST_AGENT_VER = "1.4.6" | ||||
|  | ||||
| MESH_VER = "0.7.49" | ||||
| MESH_VER = "0.7.68" | ||||
|  | ||||
| # for the update script, bump when need to recreate venv or npm install | ||||
| PIP_VER = "7" | ||||
| NPM_VER = "7" | ||||
| PIP_VER = "9" | ||||
| NPM_VER = "8" | ||||
|  | ||||
| DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe" | ||||
| DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe" | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import os | ||||
| import string | ||||
| import subprocess | ||||
| import time | ||||
| import pytz | ||||
| from typing import List, Dict | ||||
| from loguru import logger | ||||
|  | ||||
| @@ -29,6 +30,12 @@ WEEK_DAYS = { | ||||
| } | ||||
|  | ||||
|  | ||||
| def get_default_timezone(): | ||||
|     from core.models import CoreSettings | ||||
|  | ||||
|     return pytz.timezone(CoreSettings.objects.first().default_time_zone) | ||||
|  | ||||
|  | ||||
| def get_bit_days(days: List[str]) -> int: | ||||
|     bit_days = 0 | ||||
|     for day in days: | ||||
|   | ||||
| @@ -19,8 +19,11 @@ logger.configure(**settings.LOG_CONFIG) | ||||
| def auto_approve_updates_task(): | ||||
|     # scheduled task that checks and approves updates daily | ||||
|  | ||||
|     agents = Agent.objects.all() | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "version", "last_seen", "overdue_time", "offline_time" | ||||
|     ) | ||||
|     for agent in agents: | ||||
|         agent.delete_superseded_updates() | ||||
|         try: | ||||
|             agent.approve_updates() | ||||
|         except: | ||||
| @@ -43,7 +46,9 @@ def auto_approve_updates_task(): | ||||
| @app.task | ||||
| def check_agent_update_schedule_task(): | ||||
|     # scheduled task that installs updates on agents if enabled | ||||
|     agents = Agent.objects.all() | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "version", "last_seen", "overdue_time", "offline_time" | ||||
|     ) | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
| @@ -53,6 +58,7 @@ def check_agent_update_schedule_task(): | ||||
|     ] | ||||
|  | ||||
|     for agent in online: | ||||
|         agent.delete_superseded_updates() | ||||
|         install = False | ||||
|         patch_policy = agent.get_patch_policy() | ||||
|  | ||||
| @@ -126,6 +132,11 @@ def bulk_install_updates_task(pks: List[int]) -> None: | ||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             agent.delete_superseded_updates() | ||||
|             try: | ||||
|                 agent.approve_updates() | ||||
|             except: | ||||
|                 pass | ||||
|             nats_data = { | ||||
|                 "func": "installwinupdates", | ||||
|                 "guids": agent.get_approved_update_guids(), | ||||
| @@ -142,6 +153,7 @@ def bulk_check_for_updates_task(pks: List[int]) -> None: | ||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             agent.delete_superseded_updates() | ||||
|             asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) | ||||
|             time.sleep(0.05) | ||||
|         time.sleep(15) | ||||
|   | ||||
| @@ -88,7 +88,8 @@ class TestWinUpdateViews(TacticalTestCase): | ||||
|  | ||||
|     def test_edit_policy(self): | ||||
|         url = "/winupdate/editpolicy/" | ||||
|         winupdate = baker.make("winupdate.WinUpdate") | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         winupdate = baker.make("winupdate.WinUpdate", agent=agent) | ||||
|  | ||||
|         invalid_data = {"pk": 500, "policy": "inherit"} | ||||
|         # test a call where winupdate doesn't exist | ||||
|   | ||||
| @@ -22,6 +22,7 @@ def get_win_updates(request, pk): | ||||
| @api_view() | ||||
| def run_update_scan(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     agent.delete_superseded_updates() | ||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||
|         return notify_error("Requires agent version 1.3.0 or greater") | ||||
|  | ||||
| @@ -32,9 +33,11 @@ def run_update_scan(request, pk): | ||||
| @api_view() | ||||
| def install_updates(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     agent.delete_superseded_updates() | ||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||
|         return notify_error("Requires agent version 1.3.0 or greater") | ||||
|  | ||||
|     agent.approve_updates() | ||||
|     nats_data = { | ||||
|         "func": "installwinupdates", | ||||
|         "guids": agent.get_approved_update_guids(), | ||||
|   | ||||
| @@ -7,8 +7,8 @@ jobs: | ||||
|     displayName: "Setup" | ||||
|     strategy: | ||||
|       matrix: | ||||
|         Ubuntu20: | ||||
|           AGENT_NAME: "rmm-ubu20" | ||||
|         Debian10: | ||||
|           AGENT_NAME: "azpipelines-deb10" | ||||
|  | ||||
|     pool: | ||||
|       name: linux-vms | ||||
| @@ -23,11 +23,11 @@ jobs: | ||||
|  | ||||
|           rm -rf /myagent/_work/1/s/api/env | ||||
|           cd /myagent/_work/1/s/api | ||||
|           python3 -m venv env | ||||
|           python3.8 -m venv env | ||||
|           source env/bin/activate | ||||
|           cd /myagent/_work/1/s/api/tacticalrmm | ||||
|           pip install --no-cache-dir --upgrade pip | ||||
|           pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2 | ||||
|           pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2 | ||||
|           pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt | ||||
|         displayName: "Install Python Dependencies" | ||||
|  | ||||
| @@ -44,7 +44,7 @@ jobs: | ||||
|       - script: | | ||||
|           cd /myagent/_work/1/s/api | ||||
|           source env/bin/activate | ||||
|           black --check tacticalrmm | ||||
|           black --exclude migrations/ --check tacticalrmm | ||||
|           if [ $? -ne 0 ]; then | ||||
|               exit 1 | ||||
|           fi | ||||
|   | ||||
							
								
								
									
										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 | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user