743 lines
25 KiB
Python
743 lines
25 KiB
Python
import requests
|
|
import time
|
|
import base64
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Random import get_random_bytes
|
|
from Crypto.Hash import SHA3_384
|
|
from Crypto.Util.Padding import pad
|
|
import validators
|
|
import msgpack
|
|
import re
|
|
from collections import Counter
|
|
from loguru import logger
|
|
from packaging import version as pyver
|
|
from distutils.version import LooseVersion
|
|
from nats.aio.client import Client as NATS
|
|
from nats.aio.errors import ErrTimeout
|
|
|
|
from django.db import models
|
|
from django.conf import settings
|
|
from django.utils import timezone as djangotime
|
|
|
|
from core.models import CoreSettings, TZ_CHOICES
|
|
from logs.models import BaseAuditModel
|
|
|
|
logger.configure(**settings.LOG_CONFIG)
|
|
|
|
|
|
class Agent(BaseAuditModel):
|
|
version = models.CharField(default="0.1.0", max_length=255)
|
|
salt_ver = models.CharField(default="1.0.3", max_length=255)
|
|
operating_system = models.CharField(null=True, blank=True, max_length=255)
|
|
plat = models.CharField(max_length=255, null=True, blank=True)
|
|
plat_release = models.CharField(max_length=255, null=True, blank=True)
|
|
hostname = models.CharField(max_length=255)
|
|
salt_id = models.CharField(null=True, blank=True, max_length=255)
|
|
local_ip = models.TextField(null=True, blank=True) # deprecated
|
|
agent_id = models.CharField(max_length=200)
|
|
last_seen = models.DateTimeField(null=True, blank=True)
|
|
services = models.JSONField(null=True, blank=True)
|
|
public_ip = models.CharField(null=True, max_length=255)
|
|
total_ram = models.IntegerField(null=True, blank=True)
|
|
used_ram = models.IntegerField(null=True, blank=True) # deprecated
|
|
disks = models.JSONField(null=True, blank=True)
|
|
boot_time = models.FloatField(null=True, blank=True)
|
|
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
|
|
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
|
|
antivirus = models.CharField(default="n/a", max_length=255) # deprecated
|
|
monitoring_type = models.CharField(max_length=30)
|
|
description = models.CharField(null=True, blank=True, max_length=255)
|
|
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_time = models.PositiveIntegerField(default=30)
|
|
check_interval = models.PositiveIntegerField(default=120)
|
|
needs_reboot = models.BooleanField(default=False)
|
|
choco_installed = models.BooleanField(default=False)
|
|
wmi_detail = models.JSONField(null=True, blank=True)
|
|
patches_last_installed = models.DateTimeField(null=True, blank=True)
|
|
time_zone = models.CharField(
|
|
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
|
)
|
|
maintenance_mode = models.BooleanField(default=False)
|
|
site = models.ForeignKey(
|
|
"clients.Site",
|
|
related_name="agents",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
policy = models.ForeignKey(
|
|
"automation.Policy",
|
|
related_name="agents",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.hostname
|
|
|
|
@property
|
|
def client(self):
|
|
return self.site.client
|
|
|
|
@property
|
|
def has_nats(self):
|
|
return pyver.parse(self.version) >= pyver.parse("1.1.0")
|
|
|
|
@property
|
|
def has_gotasks(self):
|
|
return pyver.parse(self.version) >= pyver.parse("1.1.1")
|
|
|
|
@property
|
|
def timezone(self):
|
|
# return the default timezone unless the timezone is explicity set per agent
|
|
if self.time_zone is not None:
|
|
return self.time_zone
|
|
else:
|
|
from core.models import CoreSettings
|
|
|
|
return CoreSettings.objects.first().default_time_zone
|
|
|
|
@property
|
|
def arch(self):
|
|
if self.operating_system is not None:
|
|
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
|
|
return "64"
|
|
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
|
|
return "32"
|
|
return None
|
|
|
|
@property
|
|
def winagent_dl(self):
|
|
if self.arch == "64":
|
|
return settings.DL_64
|
|
elif self.arch == "32":
|
|
return settings.DL_32
|
|
return None
|
|
|
|
@property
|
|
def winsalt_dl(self):
|
|
if self.arch == "64":
|
|
return settings.SALT_64
|
|
elif self.arch == "32":
|
|
return settings.SALT_32
|
|
return None
|
|
|
|
@property
|
|
def win_inno_exe(self):
|
|
if self.arch == "64":
|
|
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
|
elif self.arch == "32":
|
|
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
|
return None
|
|
|
|
@property
|
|
def status(self):
|
|
offline = djangotime.now() - djangotime.timedelta(minutes=6)
|
|
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
|
|
|
|
if self.last_seen is not None:
|
|
if (self.last_seen < offline) and (self.last_seen > overdue):
|
|
return "offline"
|
|
elif (self.last_seen < offline) and (self.last_seen < overdue):
|
|
return "overdue"
|
|
else:
|
|
return "online"
|
|
else:
|
|
return "offline"
|
|
|
|
@property
|
|
def has_patches_pending(self):
|
|
return self.winupdates.filter(action="approve").filter(installed=False).exists()
|
|
|
|
@property
|
|
def checks(self):
|
|
total, passing, failing = 0, 0, 0
|
|
|
|
if self.agentchecks.exists():
|
|
for i in self.agentchecks.all():
|
|
total += 1
|
|
if i.status == "passing":
|
|
passing += 1
|
|
elif i.status == "failing":
|
|
failing += 1
|
|
|
|
ret = {
|
|
"total": total,
|
|
"passing": passing,
|
|
"failing": failing,
|
|
"has_failing_checks": failing > 0,
|
|
}
|
|
return ret
|
|
|
|
@property
|
|
def cpu_model(self):
|
|
ret = []
|
|
try:
|
|
cpus = self.wmi_detail["cpu"]
|
|
for cpu in cpus:
|
|
ret.append([x["Name"] for x in cpu if "Name" in x][0])
|
|
return ret
|
|
except:
|
|
return ["unknown cpu model"]
|
|
|
|
@property
|
|
def local_ips(self):
|
|
ret = []
|
|
try:
|
|
ips = self.wmi_detail["network_config"]
|
|
except:
|
|
return "error getting local ips"
|
|
|
|
for i in ips:
|
|
try:
|
|
addr = [x["IPAddress"] for x in i if "IPAddress" in x][0]
|
|
except:
|
|
continue
|
|
|
|
if addr is None:
|
|
continue
|
|
|
|
for ip in addr:
|
|
if validators.ipv4(ip):
|
|
ret.append(ip)
|
|
|
|
if len(ret) == 1:
|
|
return ret[0]
|
|
else:
|
|
return ", ".join(ret) if ret else "error getting local ips"
|
|
|
|
@property
|
|
def make_model(self):
|
|
try:
|
|
comp_sys = self.wmi_detail["comp_sys"][0]
|
|
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
|
|
make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
|
|
model = [x["Model"] for x in comp_sys if "Model" in x][0]
|
|
|
|
if "to be filled" in model.lower():
|
|
mobo = self.wmi_detail["base_board"][0]
|
|
make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
|
|
model = [x["Product"] for x in mobo if "Product" in x][0]
|
|
|
|
return f"{make} {model}"
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
|
|
except:
|
|
pass
|
|
|
|
return "unknown make/model"
|
|
|
|
@property
|
|
def physical_disks(self):
|
|
try:
|
|
disks = self.wmi_detail["disk"]
|
|
ret = []
|
|
for disk in disks:
|
|
interface_type = [
|
|
x["InterfaceType"] for x in disk if "InterfaceType" in x
|
|
][0]
|
|
|
|
if interface_type == "USB":
|
|
continue
|
|
|
|
model = [x["Caption"] for x in disk if "Caption" in x][0]
|
|
size = [x["Size"] for x in disk if "Size" in x][0]
|
|
|
|
size_in_gb = round(int(size) / 1_073_741_824)
|
|
ret.append(f"{model} {size_in_gb:,}GB {interface_type}")
|
|
|
|
return ret
|
|
except:
|
|
return ["unknown disk"]
|
|
|
|
# auto approves updates
|
|
def approve_updates(self):
|
|
patch_policy = self.get_patch_policy()
|
|
|
|
updates = list()
|
|
if patch_policy.critical == "approve":
|
|
updates += self.winupdates.filter(
|
|
severity="Critical", installed=False
|
|
).exclude(action="approve")
|
|
|
|
if patch_policy.important == "approve":
|
|
updates += self.winupdates.filter(
|
|
severity="Important", installed=False
|
|
).exclude(action="approve")
|
|
|
|
if patch_policy.moderate == "approve":
|
|
updates += self.winupdates.filter(
|
|
severity="Moderate", installed=False
|
|
).exclude(action="approve")
|
|
|
|
if patch_policy.low == "approve":
|
|
updates += self.winupdates.filter(severity="Low", installed=False).exclude(
|
|
action="approve"
|
|
)
|
|
|
|
if patch_policy.other == "approve":
|
|
updates += self.winupdates.filter(severity="", installed=False).exclude(
|
|
action="approve"
|
|
)
|
|
|
|
for update in updates:
|
|
update.action = "approve"
|
|
update.save(update_fields=["action"])
|
|
|
|
# returns agent policy merged with a client or site specific policy
|
|
def get_patch_policy(self):
|
|
|
|
# check if site has a patch policy and if so use it
|
|
site = self.site
|
|
core_settings = CoreSettings.objects.first()
|
|
patch_policy = None
|
|
agent_policy = self.winupdatepolicy.get()
|
|
|
|
if self.monitoring_type == "server":
|
|
# check agent policy first which should override client or site policy
|
|
if self.policy and self.policy.winupdatepolicy.exists():
|
|
patch_policy = self.policy.winupdatepolicy.get()
|
|
|
|
# check site policy if agent policy doesn't have one
|
|
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
|
|
patch_policy = site.server_policy.winupdatepolicy.get()
|
|
|
|
# if site doesn't have a patch policy check the client
|
|
elif (
|
|
site.client.server_policy
|
|
and site.client.server_policy.winupdatepolicy.exists()
|
|
):
|
|
patch_policy = site.client.server_policy.winupdatepolicy.get()
|
|
|
|
# if patch policy still doesn't exist check default policy
|
|
elif (
|
|
core_settings.server_policy
|
|
and core_settings.server_policy.winupdatepolicy.exists()
|
|
):
|
|
patch_policy = core_settings.server_policy.winupdatepolicy.get()
|
|
|
|
elif self.monitoring_type == "workstation":
|
|
# check agent policy first which should override client or site policy
|
|
if self.policy and self.policy.winupdatepolicy.exists():
|
|
patch_policy = self.policy.winupdatepolicy.get()
|
|
|
|
elif (
|
|
site.workstation_policy
|
|
and site.workstation_policy.winupdatepolicy.exists()
|
|
):
|
|
patch_policy = site.workstation_policy.winupdatepolicy.get()
|
|
|
|
# if site doesn't have a patch policy check the client
|
|
elif (
|
|
site.client.workstation_policy
|
|
and site.client.workstation_policy.winupdatepolicy.exists()
|
|
):
|
|
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
|
|
|
|
# if patch policy still doesn't exist check default policy
|
|
elif (
|
|
core_settings.workstation_policy
|
|
and core_settings.workstation_policy.winupdatepolicy.exists()
|
|
):
|
|
patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
|
|
|
|
# if policy still doesn't exist return the agent patch policy
|
|
if not patch_policy:
|
|
return agent_policy
|
|
|
|
# patch policy exists. check if any agent settings are set to override patch policy
|
|
if agent_policy.critical != "inherit":
|
|
patch_policy.critical = agent_policy.critical
|
|
|
|
if agent_policy.important != "inherit":
|
|
patch_policy.important = agent_policy.important
|
|
|
|
if agent_policy.moderate != "inherit":
|
|
patch_policy.moderate = agent_policy.moderate
|
|
|
|
if agent_policy.low != "inherit":
|
|
patch_policy.low = agent_policy.low
|
|
|
|
if agent_policy.other != "inherit":
|
|
patch_policy.other = agent_policy.other
|
|
|
|
if agent_policy.run_time_frequency != "inherit":
|
|
patch_policy.run_time_frequency = agent_policy.run_time_frequency
|
|
patch_policy.run_time_hour = agent_policy.run_time_hour
|
|
patch_policy.run_time_days = agent_policy.run_time_days
|
|
|
|
if agent_policy.reboot_after_install != "inherit":
|
|
patch_policy.reboot_after_install = agent_policy.reboot_after_install
|
|
|
|
if not agent_policy.reprocess_failed_inherit:
|
|
patch_policy.reprocess_failed = agent_policy.reprocess_failed
|
|
patch_policy.reprocess_failed_times = agent_policy.reprocess_failed_times
|
|
patch_policy.email_if_fail = agent_policy.email_if_fail
|
|
|
|
return patch_policy
|
|
|
|
# clear is used to delete managed policy checks from agent
|
|
# parent_checks specifies a list of checks to delete from agent with matching parent_check field
|
|
def generate_checks_from_policies(self, clear=False):
|
|
from automation.models import Policy
|
|
|
|
# Clear agent checks managed by policy
|
|
if clear:
|
|
self.agentchecks.filter(managed_by_policy=True).delete()
|
|
|
|
# Clear agent checks that have overriden_by_policy set
|
|
self.agentchecks.update(overriden_by_policy=False)
|
|
|
|
# Generate checks based on policies
|
|
Policy.generate_policy_checks(self)
|
|
|
|
# clear is used to delete managed policy tasks from agent
|
|
# parent_tasks specifies a list of tasks to delete from agent with matching parent_task field
|
|
def generate_tasks_from_policies(self, clear=False):
|
|
from autotasks.tasks import delete_win_task_schedule
|
|
from automation.models import Policy
|
|
|
|
# Clear agent tasks managed by policy
|
|
if clear:
|
|
for task in self.autotasks.filter(managed_by_policy=True):
|
|
delete_win_task_schedule.delay(task.pk)
|
|
|
|
# Generate tasks based on policies
|
|
Policy.generate_policy_tasks(self)
|
|
|
|
# https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347
|
|
def get_login_token(self, key, user, action=3):
|
|
try:
|
|
key = bytes.fromhex(key)
|
|
key1 = key[0:48]
|
|
key2 = key[48:]
|
|
msg = '{{"a":{}, "u":"{}","time":{}}}'.format(
|
|
action, user, int(time.time())
|
|
)
|
|
iv = get_random_bytes(16)
|
|
|
|
# sha
|
|
h = SHA3_384.new()
|
|
h.update(key1)
|
|
hashed_msg = h.digest() + msg.encode()
|
|
|
|
# aes
|
|
cipher = AES.new(key2, AES.MODE_CBC, iv)
|
|
msg = cipher.encrypt(pad(hashed_msg, 16))
|
|
|
|
return base64.b64encode(iv + msg, altchars=b"@$").decode("utf-8")
|
|
except Exception:
|
|
return "err"
|
|
|
|
async def nats_cmd(self, data, timeout=30, wait=True):
|
|
nc = NATS()
|
|
options = {
|
|
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
|
"user": "tacticalrmm",
|
|
"password": settings.SECRET_KEY,
|
|
"connect_timeout": 3,
|
|
"max_reconnect_attempts": 2,
|
|
}
|
|
try:
|
|
await nc.connect(**options)
|
|
except:
|
|
return "natsdown"
|
|
|
|
if wait:
|
|
try:
|
|
msg = await nc.request(
|
|
self.agent_id, msgpack.dumps(data), timeout=timeout
|
|
)
|
|
except ErrTimeout:
|
|
ret = "timeout"
|
|
else:
|
|
ret = msgpack.loads(msg.data)
|
|
|
|
await nc.close()
|
|
return ret
|
|
else:
|
|
await nc.publish(self.agent_id, msgpack.dumps(data))
|
|
await nc.flush()
|
|
await nc.close()
|
|
|
|
def salt_api_cmd(self, **kwargs):
|
|
|
|
# salt should always timeout first before the requests' timeout
|
|
try:
|
|
timeout = kwargs["timeout"]
|
|
except KeyError:
|
|
# default timeout
|
|
timeout = 15
|
|
salt_timeout = 12
|
|
else:
|
|
if timeout < 8:
|
|
timeout = 8
|
|
salt_timeout = 5
|
|
else:
|
|
salt_timeout = timeout - 3
|
|
|
|
json = {
|
|
"client": "local",
|
|
"tgt": self.salt_id,
|
|
"fun": kwargs["func"],
|
|
"timeout": salt_timeout,
|
|
"username": settings.SALT_USERNAME,
|
|
"password": settings.SALT_PASSWORD,
|
|
"eauth": "pam",
|
|
}
|
|
|
|
if "arg" in kwargs:
|
|
json.update({"arg": kwargs["arg"]})
|
|
if "kwargs" in kwargs:
|
|
json.update({"kwarg": kwargs["kwargs"]})
|
|
|
|
try:
|
|
resp = requests.post(
|
|
f"http://{settings.SALT_HOST}:8123/run",
|
|
json=[json],
|
|
timeout=timeout,
|
|
)
|
|
except Exception:
|
|
return "timeout"
|
|
|
|
try:
|
|
ret = resp.json()["return"][0][self.salt_id]
|
|
except Exception as e:
|
|
logger.error(f"{self.salt_id}: {e}")
|
|
return "error"
|
|
else:
|
|
return ret
|
|
|
|
def salt_api_async(self, **kwargs):
|
|
|
|
json = {
|
|
"client": "local_async",
|
|
"tgt": self.salt_id,
|
|
"fun": kwargs["func"],
|
|
"username": settings.SALT_USERNAME,
|
|
"password": settings.SALT_PASSWORD,
|
|
"eauth": "pam",
|
|
}
|
|
|
|
if "arg" in kwargs:
|
|
json.update({"arg": kwargs["arg"]})
|
|
if "kwargs" in kwargs:
|
|
json.update({"kwarg": kwargs["kwargs"]})
|
|
|
|
try:
|
|
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
|
|
except Exception:
|
|
return "timeout"
|
|
|
|
return resp
|
|
|
|
@staticmethod
|
|
def serialize(agent):
|
|
# serializes the agent and returns json
|
|
from .serializers import AgentEditSerializer
|
|
|
|
ret = AgentEditSerializer(agent).data
|
|
del ret["all_timezones"]
|
|
del ret["client"]
|
|
return ret
|
|
|
|
@staticmethod
|
|
def salt_batch_async(**kwargs):
|
|
assert isinstance(kwargs["minions"], list)
|
|
|
|
json = {
|
|
"client": "local_async",
|
|
"tgt_type": "list",
|
|
"tgt": kwargs["minions"],
|
|
"fun": kwargs["func"],
|
|
"username": settings.SALT_USERNAME,
|
|
"password": settings.SALT_PASSWORD,
|
|
"eauth": "pam",
|
|
}
|
|
|
|
if "arg" in kwargs:
|
|
json.update({"arg": kwargs["arg"]})
|
|
if "kwargs" in kwargs:
|
|
json.update({"kwarg": kwargs["kwargs"]})
|
|
|
|
try:
|
|
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
|
|
except Exception:
|
|
return "timeout"
|
|
|
|
return resp
|
|
|
|
def delete_superseded_updates(self):
|
|
try:
|
|
pks = [] # list of pks to delete
|
|
kbs = list(self.winupdates.values_list("kb", flat=True))
|
|
d = Counter(kbs)
|
|
dupes = [k for k, v in d.items() if v > 1]
|
|
|
|
for dupe in dupes:
|
|
titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
|
|
# extract the version from the title and sort from oldest to newest
|
|
# skip if no version info is available therefore nothing to parse
|
|
try:
|
|
vers = [
|
|
re.search(r"\(Version(.*?)\)", i).group(1).strip()
|
|
for i in titles
|
|
]
|
|
sorted_vers = sorted(vers, key=LooseVersion)
|
|
except:
|
|
continue
|
|
# append all but the latest version to our list of pks to delete
|
|
for ver in sorted_vers[:-1]:
|
|
q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
|
|
pks.append(q.first().pk)
|
|
|
|
pks = list(set(pks))
|
|
self.winupdates.filter(pk__in=pks).delete()
|
|
except:
|
|
pass
|
|
|
|
# define how the agent should handle pending actions
|
|
def handle_pending_actions(self):
|
|
pending_actions = self.pendingactions.filter(status="pending")
|
|
|
|
for action in pending_actions:
|
|
if action.action_type == "taskaction":
|
|
from autotasks.tasks import (
|
|
create_win_task_schedule,
|
|
enable_or_disable_win_task,
|
|
delete_win_task_schedule,
|
|
)
|
|
|
|
task_id = action.details["task_id"]
|
|
|
|
if action.details["action"] == "taskcreate":
|
|
create_win_task_schedule.delay(task_id, pending_action=action.id)
|
|
elif action.details["action"] == "tasktoggle":
|
|
enable_or_disable_win_task.delay(
|
|
task_id, action.details["value"], pending_action=action.id
|
|
)
|
|
elif action.details["action"] == "taskdelete":
|
|
delete_win_task_schedule.delay(task_id, pending_action=action.id)
|
|
|
|
|
|
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)
|
|
|
|
@property
|
|
def is_active(self):
|
|
return False if self.recovery_time else True
|
|
|
|
def send_outage_email(self):
|
|
from core.models import CoreSettings
|
|
|
|
CORE = CoreSettings.objects.first()
|
|
CORE.send_mail(
|
|
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.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} "
|
|
"within the expected time."
|
|
),
|
|
)
|
|
|
|
def send_recovery_email(self):
|
|
from core.models import CoreSettings
|
|
|
|
CORE = CoreSettings.objects.first()
|
|
CORE.send_mail(
|
|
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.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} "
|
|
"after an interruption in data transmission."
|
|
),
|
|
)
|
|
|
|
def send_outage_sms(self):
|
|
from core.models import CoreSettings
|
|
|
|
CORE = CoreSettings.objects.first()
|
|
CORE.send_sms(
|
|
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue"
|
|
)
|
|
|
|
def send_recovery_sms(self):
|
|
from core.models import CoreSettings
|
|
|
|
CORE = CoreSettings.objects.first()
|
|
CORE.send_sms(
|
|
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received"
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.agent.hostname
|
|
|
|
|
|
RECOVERY_CHOICES = [
|
|
("salt", "Salt"),
|
|
("mesh", "Mesh"),
|
|
("command", "Command"),
|
|
("rpc", "Nats RPC"),
|
|
("checkrunner", "Checkrunner"),
|
|
]
|
|
|
|
|
|
class RecoveryAction(models.Model):
|
|
agent = models.ForeignKey(
|
|
Agent,
|
|
related_name="recoveryactions",
|
|
on_delete=models.CASCADE,
|
|
)
|
|
mode = models.CharField(max_length=50, choices=RECOVERY_CHOICES, default="mesh")
|
|
command = models.TextField(null=True, blank=True)
|
|
last_run = models.DateTimeField(null=True, blank=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.agent.hostname} - {self.mode}"
|
|
|
|
def send(self):
|
|
ret = {"recovery": self.mode}
|
|
if self.mode == "command":
|
|
ret["cmd"] = self.command
|
|
return ret
|
|
|
|
|
|
class Note(models.Model):
|
|
agent = models.ForeignKey(
|
|
Agent,
|
|
related_name="notes",
|
|
on_delete=models.CASCADE,
|
|
)
|
|
user = models.ForeignKey(
|
|
"accounts.User",
|
|
related_name="user",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
note = models.TextField(null=True, blank=True)
|
|
entry_time = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return self.agent.hostname
|