Skip to content

Commit

Permalink
Release 0.4.22
Browse files Browse the repository at this point in the history
  • Loading branch information
wh1te909 committed Mar 5, 2021
2 parents 501c04a + 880d825 commit 72d55a0
Show file tree
Hide file tree
Showing 29 changed files with 417 additions and 173 deletions.
20 changes: 20 additions & 0 deletions api/tacticalrmm/agents/migrations/0031_agent_alert_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-03-04 03:57

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('alerts', '0006_auto_20210217_1736'),
('agents', '0030_agent_offline_time'),
]

operations = [
migrations.AddField(
model_name='agent',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
),
]
39 changes: 24 additions & 15 deletions api/tacticalrmm/agents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from nats.aio.errors import ErrTimeout
from packaging import version as pyver

from alerts.models import AlertTemplate
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel

Expand Down Expand Up @@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
site = models.ForeignKey(
"clients.Site",
related_name="agents",
Expand All @@ -85,7 +91,7 @@ def save(self, *args, **kwargs):
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
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
if (
Expand Down Expand Up @@ -461,9 +467,9 @@ def get_approved_update_guids(self) -> list[str]:
)
)

# 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]:
# sets alert template assigned in the following order: policy, site, client, global
# sets None if nothing is found
def set_alert_template(self):

site = self.site
client = self.client
Expand Down Expand Up @@ -563,9 +569,16 @@ def get_alert_template(self) -> Union[AlertTemplate, None]:
continue

else:
# save alert_template to agent cache field
self.alert_template = template
self.save()

return template

# no alert templates found or agent has been excluded
self.alert_template = None
self.save()

return None

def generate_checks_from_policies(self):
Expand Down Expand Up @@ -703,11 +716,11 @@ def handle_pending_actions(self):
# for clearing duplicate pending actions on agent
def remove_matching_pending_task_actions(self, task_id):
# remove any other pending actions on agent with same task_id
for action in self.pendingactions.exclude(status="completed"): # type: ignore
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
if action.details["task_id"] == task_id:
action.delete()

def should_create_alert(self, alert_template):
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert
or self.overdue_email_alert
Expand All @@ -726,7 +739,6 @@ 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.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
Expand All @@ -735,14 +747,13 @@ def send_outage_email(self):
f"agent {self.hostname} "
"within the expected time."
),
alert_template=alert_template,
alert_template=self.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.client.name}, {self.site.name}, {self.hostname} - data received",
(
Expand All @@ -751,27 +762,25 @@ def send_recovery_email(self):
f"agent {self.hostname} "
"after an interruption in data transmission."
),
alert_template=alert_template,
alert_template=self.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.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=alert_template,
alert_template=self.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.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
alert_template=self.alert_template,
)


Expand Down
11 changes: 5 additions & 6 deletions api/tacticalrmm/agents/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
alert_template = serializers.SerializerMethodField()

def get_alert_template(self, obj):
alert_template = obj.get_alert_template()

if not alert_template:
if not obj.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,
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}

def get_pending_actions(self, obj):
Expand Down
14 changes: 0 additions & 14 deletions api/tacticalrmm/agents/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,20 +200,6 @@ def agent_outages_task() -> None:
Alert.handle_alert_failure(agent)


@app.task
def handle_agent_recovery_task(pk: int) -> None:
sleep(10)
from agents.models import RecoveryAction

action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}

asyncio.run(action.agent.nats_cmd(data, wait=False))


@app.task
def run_script_email_results_task(
agentpk: int,
Expand Down
65 changes: 46 additions & 19 deletions api/tacticalrmm/agents/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,42 +421,69 @@ def test_install_agent(self, mock_subprocess, mock_file_exists):

self.check_not_authenticated("post", url)

def test_recover(self):
@patch("agents.models.Agent.nats_cmd")
def test_recover(self, nats_cmd):
from agents.models import RecoveryAction

self.agent.version = "0.11.1"
self.agent.save(update_fields=["version"])
RecoveryAction.objects.all().delete()
url = "/agents/recover/"
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
agent = baker.make_recipe("agents.online_agent")

# test mesh realtime
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
)
nats_cmd.reset_mock()

data["mode"] = "mesh"
# test mesh with agent rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json())

self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 1)
mesh_recovery = RecoveryAction.objects.first()
self.assertEqual(mesh_recovery.mode, "mesh")
nats_cmd.reset_mock()
RecoveryAction.objects.all().delete()
data["mode"] = "command"
data["cmd"] = "ipconfig /flushdns"

# test tacagent realtime
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
)
nats_cmd.reset_mock()

RecoveryAction.objects.all().delete()
data["cmd"] = None
# test tacagent with rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.reset_mock()

RecoveryAction.objects.all().delete()

self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"])
data["mode"] = "mesh"
# test shell cmd without command
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json())
self.assertEqual(RecoveryAction.objects.count(), 0)

self.check_not_authenticated("post", url)
# test shell cmd
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 1)
cmd_recovery = RecoveryAction.objects.first()
self.assertEqual(cmd_recovery.mode, "command")
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")

def test_agents_agent_detail(self):
url = f"/agents/{self.agent.pk}/agentdetail/"
Expand Down
32 changes: 13 additions & 19 deletions api/tacticalrmm/agents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,26 +228,28 @@ class AgentsTableList(APIView):
def patch(self, request):
if "sitePK" in request.data.keys():
queryset = (
Agent.objects.select_related("site")
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site_id=request.data["sitePK"])
)
elif "clientPK" in request.data.keys():
queryset = (
Agent.objects.select_related("site")
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site__client_id=request.data["clientPK"])
)
else:
queryset = Agent.objects.select_related("site").prefetch_related(
"agentchecks"
)
queryset = Agent.objects.select_related(
"site", "policy", "alert_template"
).prefetch_related("agentchecks")

queryset = queryset.only(
"pk",
"hostname",
"agent_id",
"site",
"policy",
"alert_template",
"monitoring_type",
"description",
"needs_reboot",
Expand Down Expand Up @@ -487,20 +489,12 @@ def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
mode = request.data["mode"]

if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
return notify_error("Only available in agent version greater than 0.9.5")

if not agent.has_nats:
if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")

# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
# attempt a realtime recovery, otherwise fall back to old recovery method
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")

if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
return notify_error(
Expand Down
12 changes: 6 additions & 6 deletions api/tacticalrmm/alerts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
email_alert = instance.overdue_email_alert
text_alert = instance.overdue_text_alert
dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.get_alert_template()
alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode
alert_severity = "error"
agent = instance
Expand Down Expand Up @@ -194,7 +194,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
Expand Down Expand Up @@ -227,7 +227,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
Expand Down Expand Up @@ -336,7 +336,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
resolved_email_task = agent_recovery_email_task
resolved_text_task = agent_recovery_sms_task

alert_template = instance.get_alert_template()
alert_template = instance.alert_template
alert = cls.objects.get(agent=instance, resolved=False)
maintenance_mode = instance.maintenance_mode
agent = instance
Expand All @@ -354,7 +354,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
resolved_email_task = handle_resolved_check_email_alert_task
resolved_text_task = handle_resolved_check_sms_alert_task

alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_check=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
Expand All @@ -372,7 +372,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N
resolved_email_task = handle_resolved_task_email_alert
resolved_text_task = handle_resolved_task_sms_alert

alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_task=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
Expand Down
Loading

0 comments on commit 72d55a0

Please sign in to comment.