Files
tacticalrmm/api/tacticalrmm/autotasks/models.py
2022-04-21 06:34:06 +00:00

605 lines
22 KiB
Python

import asyncio
import random
import string
import pytz
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Union
from alerts.models import SEVERITY_CHOICES
from django.core.validators import MaxValueValidator, MinValueValidator
from django.core.cache import cache
from django.utils import timezone as djangotime
from django.db import models
from django.db.models.fields import DateTimeField
from django.db.models.fields.json import JSONField
from django.db.utils import DatabaseError
from logs.models import BaseAuditModel, DebugLog
from core.utils import get_core_settings
from tacticalrmm.constants import (
FIELDS_TRIGGER_TASK_UPDATE_AGENT,
POLICY_TASK_FIELDS_TO_COPY,
)
if TYPE_CHECKING:
from automation.models import Policy
from alerts.models import Alert, AlertTemplate
from agents.models import Agent
from checks.models import Check
from tacticalrmm.models import PermissionQuerySet
from tacticalrmm.utils import (
bitdays_to_string,
bitmonthdays_to_string,
bitmonths_to_string,
bitweeks_to_string,
convert_to_iso_duration,
)
TASK_TYPE_CHOICES = [
("daily", "Daily"),
("weekly", "Weekly"),
("monthly", "Monthly"),
("monthlydow", "Monthly Day of Week"),
("checkfailure", "On Check Failure"),
("manual", "Manual"),
("runonce", "Run Once"),
("scheduled", "Scheduled"), # deprecated
]
SYNC_STATUS_CHOICES = [
("synced", "Synced With Agent"),
("notsynced", "Waiting On Agent Checkin"),
("pendingdeletion", "Pending Deletion on Agent"),
("initial", "Initial Task Sync"),
]
TASK_STATUS_CHOICES = [
("passing", "Passing"),
("failing", "Failing"),
("pending", "Pending"),
]
def generate_task_name() -> str:
chars = string.ascii_letters
return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35))
class AutomatedTask(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="autotasks",
on_delete=models.CASCADE,
null=True,
blank=True,
)
policy = models.ForeignKey(
"automation.Policy",
related_name="autotasks",
null=True,
blank=True,
on_delete=models.CASCADE,
)
custom_field = models.ForeignKey(
"core.CustomField",
related_name="autotasks",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
# format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]
actions = JSONField(default=list)
assigned_check = models.ForeignKey(
"checks.Check",
null=True,
blank=True,
related_name="assignedtasks",
on_delete=models.SET_NULL,
)
name = models.CharField(max_length=255)
collector_all_output = models.BooleanField(default=False)
enabled = models.BooleanField(default=True)
continue_on_error = models.BooleanField(default=True)
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)
# options sent to agent for task creation
# general task settings
task_type = models.CharField(
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
)
win_task_name = models.CharField(
max_length=255, unique=True, blank=True, default=generate_task_name
) # should be changed to unique=True
run_time_date = DateTimeField(null=True, blank=True)
expire_date = DateTimeField(null=True, blank=True)
# daily
daily_interval = models.PositiveSmallIntegerField(
blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(255)]
)
# weekly
run_time_bit_weekdays = models.IntegerField(null=True, blank=True)
weekly_interval = models.PositiveSmallIntegerField(
blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(52)]
)
run_time_minute = models.CharField(
max_length=5, null=True, blank=True
) # deprecated
# monthly
monthly_days_of_month = models.PositiveBigIntegerField(blank=True, null=True)
monthly_months_of_year = models.PositiveIntegerField(blank=True, null=True)
# monthly days of week
monthly_weeks_of_month = models.PositiveSmallIntegerField(blank=True, null=True)
# additional task settings
task_repetition_duration = models.CharField(max_length=10, null=True, blank=True)
task_repetition_interval = models.CharField(max_length=10, null=True, blank=True)
stop_task_at_duration_end = models.BooleanField(blank=True, default=False)
random_task_delay = models.CharField(max_length=10, null=True, blank=True)
remove_if_not_scheduled = models.BooleanField(default=False)
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1)
# deprecated
managed_by_policy = models.BooleanField(default=False)
# non-database property
task_result: "Union[TaskResult, Dict[None, None]]" = {}
def __str__(self) -> str:
return self.name
def save(self, *args, **kwargs) -> None:
# if task is a policy task clear cache on everything
if self.policy:
cache.delete_many_pattern("site_*_tasks")
cache.delete_many_pattern("agent_*_tasks")
# get old task if exists
old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
# check if fields were updated that require a sync to the agent and set status to notsynced
if old_task:
for field in self.fields_that_trigger_task_update_on_agent:
if getattr(self, field) != getattr(old_task, field):
if self.policy:
TaskResult.objects.exclude(sync_status="initial").filter(
task__policy_id=self.policy.id
).update(sync_status="notsynced")
else:
TaskResult.objects.filter(agent=self.agent, task=self).update(
sync_status="notsynced"
)
def delete(self, *args, **kwargs):
# if task is a policy task clear cache on everything
if self.policy:
cache.delete_many_pattern("site_*_tasks")
cache.delete_many_pattern("agent_*_tasks")
super(AutomatedTask, self).delete(
*args,
**kwargs,
)
@property
def schedule(self) -> Optional[str]:
if self.task_type == "manual":
return "Manual"
elif self.task_type == "checkfailure":
return "Every time check fails"
elif self.task_type == "runonce":
return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}'
elif self.task_type == "daily":
run_time_nice = self.run_time_date.strftime("%I:%M%p")
if self.daily_interval == 1:
return f"Daily at {run_time_nice}"
else:
return f"Every {self.daily_interval} days at {run_time_nice}"
elif self.task_type == "weekly":
run_time_nice = self.run_time_date.strftime("%I:%M%p")
days = bitdays_to_string(self.run_time_bit_weekdays)
if self.weekly_interval != 1:
return f"{days} at {run_time_nice}"
else:
return f"{days} at {run_time_nice} every {self.weekly_interval} weeks"
elif self.task_type == "monthly":
run_time_nice = self.run_time_date.strftime("%I:%M%p")
months = bitmonths_to_string(self.monthly_months_of_year)
days = bitmonthdays_to_string(self.monthly_days_of_month)
return f"Runs on {months} on days {days} at {run_time_nice}"
elif self.task_type == "monthlydow":
run_time_nice = self.run_time_date.strftime("%I:%M%p")
months = bitmonths_to_string(self.monthly_months_of_year)
weeks = bitweeks_to_string(self.monthly_weeks_of_month)
days = bitdays_to_string(self.run_time_bit_weekdays)
return f"Runs on {months} on {weeks} on {days} at {run_time_nice}"
@property
def fields_that_trigger_task_update_on_agent(self) -> List[str]:
return FIELDS_TRIGGER_TASK_UPDATE_AGENT
@staticmethod
def serialize(task):
# serializes the task and returns json
from .serializers import TaskAuditSerializer
return TaskAuditSerializer(task).data
def create_policy_task(
self, policy: "Policy", assigned_check: "Optional[Check]" = None
) -> None:
### Copies certain properties on this task (self) to a new task and sets it to the supplied Policy
fields_to_copy = POLICY_TASK_FIELDS_TO_COPY
task = AutomatedTask.objects.create(
policy=policy,
assigned_check=assigned_check,
)
for field in fields_to_copy:
setattr(task, field, getattr(self, field))
task.save()
# agent version >= 1.8.0
def generate_nats_task_payload(
self, agent: "Optional[Agent]" = None, editing: bool = False
) -> Dict[str, Any]:
task = {
"pk": self.pk,
"type": "rmm",
"name": self.win_task_name,
"overwrite_task": editing,
"enabled": self.enabled,
"trigger": self.task_type if self.task_type != "checkfailure" else "manual",
"multiple_instances": self.task_instance_policy
if self.task_instance_policy
else 0,
"delete_expired_task_after": self.remove_if_not_scheduled
if self.expire_date
else False,
"start_when_available": self.run_asap_after_missed
if self.task_type != "runonce"
else True,
}
if self.task_type in ["runonce", "daily", "weekly", "monthly", "monthlydow"]:
# set runonce task in future if creating and run_asap_after_missed is set
if (
not editing
and self.task_type == "runonce"
and self.run_asap_after_missed
and agent
and self.run_time_date
< djangotime.now().astimezone(pytz.timezone(agent.timezone))
):
self.run_time_date = (
djangotime.now() + djangotime.timedelta(minutes=5)
).astimezone(pytz.timezone(agent.timezone))
task["start_year"] = int(self.run_time_date.strftime("%Y"))
task["start_month"] = int(self.run_time_date.strftime("%-m"))
task["start_day"] = int(self.run_time_date.strftime("%-d"))
task["start_hour"] = int(self.run_time_date.strftime("%-H"))
task["start_min"] = int(self.run_time_date.strftime("%-M"))
if self.expire_date:
task["expire_year"] = int(self.expire_date.strftime("%Y"))
task["expire_month"] = int(self.expire_date.strftime("%-m"))
task["expire_day"] = int(self.expire_date.strftime("%-d"))
task["expire_hour"] = int(self.expire_date.strftime("%-H"))
task["expire_min"] = int(self.expire_date.strftime("%-M"))
if self.random_task_delay:
task["random_delay"] = convert_to_iso_duration(self.random_task_delay)
if self.task_repetition_interval:
task["repetition_interval"] = convert_to_iso_duration(
self.task_repetition_interval
)
task["repetition_duration"] = convert_to_iso_duration(
self.task_repetition_duration
)
task["stop_at_duration_end"] = self.stop_task_at_duration_end
if self.task_type == "daily":
task["day_interval"] = self.daily_interval
elif self.task_type == "weekly":
task["week_interval"] = self.weekly_interval
task["days_of_week"] = self.run_time_bit_weekdays
elif self.task_type == "monthly":
# check if "last day is configured"
if self.monthly_days_of_month >= 0x80000000:
task["days_of_month"] = self.monthly_days_of_month - 0x80000000
task["run_on_last_day_of_month"] = True
else:
task["days_of_month"] = self.monthly_days_of_month
task["run_on_last_day_of_month"] = False
task["months_of_year"] = self.monthly_months_of_year
elif self.task_type == "monthlydow":
task["days_of_week"] = self.run_time_bit_weekdays
task["months_of_year"] = self.monthly_months_of_year
task["weeks_of_month"] = self.monthly_weeks_of_month
return task
def create_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
if self.policy and not agent:
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try:
task_result = TaskResult.objects.get(agent=agent, task=self)
except TaskResult.DoesNotExist:
task_result = TaskResult(agent=agent, task=self)
task_result.save()
nats_data = {
"func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(agent),
}
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
task_result.sync_status = "initial"
task_result.save(update_fields=["sync_status"])
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}. It will be created when the agent checks in.",
)
return "timeout"
else:
task_result.sync_status = "synced"
task_result.save(update_fields=["sync_status"])
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} was successfully created",
)
return "ok"
def modify_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
if self.policy and not agent:
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try:
task_result = TaskResult.objects.get(agent=agent, task=self)
except TaskResult.DoesNotExist:
task_result = TaskResult(agent=agent, task=self)
task_result.save()
nats_data = {
"func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(editing=True),
}
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
task_result.sync_status = "notsynced"
task_result.save(update_fields=["sync_status"])
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}({task_result.agent.agent_id}). It will try again on next agent checkin",
)
return "timeout"
else:
task_result.sync_status = "synced"
task_result.save(update_fields=["sync_status"])
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} was successfully modified",
)
return "ok"
def delete_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
if self.policy and not agent:
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try:
task_result = TaskResult.objects.get(agent=agent, task=self)
except TaskResult.DoesNotExist:
task_result = TaskResult(agent=agent, task=self)
task_result.save()
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name},
}
r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r:
task_result.sync_status = "pendingdeletion"
try:
task_result.save(update_fields=["sync_status"])
except DatabaseError:
pass
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin",
)
return "timeout"
else:
self.delete()
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted",
)
return "ok"
def run_win_task(self, agent: "Optional[Agent]" = None) -> str:
if self.policy and not agent:
return "agent parameter needs to be passed with policy task"
else:
agent = agent if self.policy else self.agent
try:
task_result = TaskResult.objects.get(agent=agent, task=self)
except TaskResult.DoesNotExist:
task_result = TaskResult(agent=agent, task=self)
task_result.save()
asyncio.run(
task_result.agent.nats_cmd(
{"func": "runtask", "taskpk": self.pk}, wait=False
)
)
return "ok"
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.task_always_alert
or alert_template.task_always_email
or alert_template.task_always_text
)
)
)
class TaskResult(models.Model):
class Meta:
unique_together = (("agent", "task"),)
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="taskresults",
on_delete=models.CASCADE,
)
task = models.ForeignKey(
"autotasks.AutomatedTask",
related_name="taskresults",
on_delete=models.CASCADE,
)
retcode = models.IntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
execution_time = models.CharField(max_length=100, default="0.0000")
last_run = models.DateTimeField(null=True, blank=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="initial"
)
def __str__(self):
return f"{self.agent.hostname} - {self.task}"
def get_or_create_alert_if_needed(
self, alert_template: "Optional[AlertTemplate]"
) -> "Optional[Alert]":
from alerts.models import Alert
return Alert.create_or_return_task_alert(
self.task,
agent=self.agent,
skip_create=not self.task.should_create_alert(alert_template),
)
def save_collector_results(self) -> None:
agent_field = self.task.custom_field.get_or_create_field_value(self.agent)
value = (
self.stdout.strip()
if self.task.collector_all_output
else self.stdout.strip().split("\n")[-1].strip()
)
agent_field.save_to_field(value)
def send_email(self):
CORE = get_core_settings()
# Format of Email sent when Task has email alert
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {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, self.agent.alert_template)
def send_sms(self):
CORE = get_core_settings()
# Format of SMS sent when Task has SMS alert
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {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=self.agent.alert_template)
def send_resolved_email(self):
CORE = get_core_settings()
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=self.agent.alert_template)
def send_resolved_sms(self):
CORE = get_core_settings()
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=self.agent.alert_template)