Compare commits

...

25 Commits

Author SHA1 Message Date
wh1te909
b5c28de03f Release 0.4.2 2021-01-29 08:23:06 +00:00
wh1te909
e17d25c156 bump versions 2021-01-29 08:12:03 +00:00
wh1te909
c25dc1b99c also override shell during load community scripts 2021-01-29 07:39:08 +00:00
Tragic Bronson
a493a574bd Merge pull request #265 from saulens22/patch-1
Fix "TRMM Defender Exclusions" script shell type
2021-01-28 23:36:03 -08:00
Saulius Kazokas
4284493dce Fix "TRMM Defender Exclusions" script shell type 2021-01-29 07:10:10 +02:00
wh1te909
25059de8e1 fix superseded windows defender updates 2021-01-29 02:37:51 +00:00
wh1te909
1731b05ad0 remove old serializers 2021-01-29 02:25:31 +00:00
wh1te909
e80dc663ac remove unused func 2021-01-29 02:22:06 +00:00
wh1te909
39988a4c2f cleanup an old view 2021-01-29 02:15:27 +00:00
wh1te909
415bff303a add some debug for unsupported agents 2021-01-29 01:22:35 +00:00
wh1te909
a65eb62a54 checkrunner changes wh1te909/rmmagent@10a0935f1b 2021-01-29 00:34:18 +00:00
wh1te909
03b2982128 update build flags 2021-01-28 23:11:32 +00:00
wh1te909
bff0527857 Release 0.4.1 2021-01-27 07:48:14 +00:00
wh1te909
f3b7634254 fix tests 2021-01-27 07:45:00 +00:00
wh1te909
6a9593c0b9 bump versions 2021-01-27 07:35:11 +00:00
wh1te909
edb785b8e5 prepare for agent 1.4.0 2021-01-27 07:11:49 +00:00
wh1te909
26d757b50a checkrunner interval changes wh1te909/rmmagent@7f131d54cf 2021-01-27 06:38:42 +00:00
wh1te909
535079ee87 update natsapi 2021-01-26 20:54:30 +00:00
wh1te909
ac380c29c1 fix last response sorting closes #258 2021-01-26 19:58:08 +00:00
wh1te909
3fd212f26c more optimizations 2021-01-25 21:05:59 +00:00
wh1te909
04a3abc651 fix tests 2021-01-25 20:46:22 +00:00
wh1te909
6caf85ddd1 optimize some queries 2021-01-25 20:27:20 +00:00
wh1te909
16e4071508 use error msg from backend 2021-01-25 19:57:50 +00:00
wh1te909
69e7c4324b start mkdocs 2021-01-25 19:55:48 +00:00
wh1te909
a1c4a8cbe5 fix tab refresh 2021-01-23 06:27:33 +00:00
49 changed files with 384 additions and 11137 deletions

View File

@@ -34,6 +34,12 @@ class AgentSerializer(serializers.ModelSerializer):
] ]
class AgentOverdueActionSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = ["pk", "overdue_email_alert", "overdue_text_alert"]
class AgentTableSerializer(serializers.ModelSerializer): class AgentTableSerializer(serializers.ModelSerializer):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending") patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField() pending_actions = serializers.SerializerMethodField()
@@ -54,7 +60,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
else: else:
agent_tz = self.context["default_tz"] 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: def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == "online": if obj.logged_in_username == "None" and obj.status == "online":

View File

@@ -19,9 +19,11 @@ logger.configure(**settings.LOG_CONFIG)
def _check_agent_service(pk: int) -> None: def _check_agent_service(pk: int) -> None:
agent = Agent.objects.get(pk=pk) agent = Agent.objects.get(pk=pk)
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
# if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
# then tacticalagent service is hung. forcefully restart it
if r == "pong": if r == "pong":
logger.info( logger.info(
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery" f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
) )
data = {"func": "recover", "payload": {"mode": "tacagent"}} data = {"func": "recover", "payload": {"mode": "tacagent"}}
asyncio.run(agent.nats_cmd(data, wait=False)) asyncio.run(agent.nats_cmd(data, wait=False))
@@ -49,7 +51,7 @@ def check_in_task() -> None:
@app.task @app.task
def monitor_agents_task() -> None: def monitor_agents_task() -> None:
q = Agent.objects.all() q = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"] agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
for agent in agents: for agent in agents:
_check_agent_service(agent) _check_agent_service(agent)
@@ -62,9 +64,18 @@ def agent_update(pk: int) -> str:
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
return "noarch" return "noarch"
version = settings.LATEST_AGENT_VER # removed sqlite in 1.4.0 to get rid of cgo dependency
url = agent.winagent_dl # 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
inno = agent.win_inno_exe 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 agent.has_nats:
if pyver.parse(agent.version) <= pyver.parse("1.1.11"): if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
@@ -100,6 +111,10 @@ def agent_update(pk: int) -> str:
asyncio.run(agent.nats_cmd(nats_data, wait=False)) asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created" return "created"
else:
logger.warning(
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to update."
)
return "not supported" return "not supported"
@@ -141,7 +156,7 @@ def auto_self_agent_update_task() -> None:
@app.task @app.task
def get_wmi_task(): def get_wmi_task():
agents = Agent.objects.all() agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [ online = [
i i
for i in agents for i in agents
@@ -158,7 +173,7 @@ def get_wmi_task():
@app.task @app.task
def sync_sysinfo_task(): def sync_sysinfo_task():
agents = Agent.objects.all() agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [ online = [
i i
for i in agents for i in agents
@@ -307,7 +322,7 @@ def remove_salt_task() -> None:
if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT: if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT:
return return
q = Agent.objects.all() q = Agent.objects.only("pk", "version")
agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] 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)) chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
for chunk in chunks: for chunk in chunks:

View File

@@ -479,42 +479,20 @@ class TestAgentViews(TacticalTestCase):
def test_overdue_action(self): def test_overdue_action(self):
url = "/agents/overdueaction/" 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") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk) agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert) self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data) self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "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_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"})
r = self.client.post(url, payload, format="json") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk) agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert) self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data) 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) self.check_not_authenticated("post", url)
def test_list_agents_no_detail(self): def test_list_agents_no_detail(self):
@@ -780,19 +758,20 @@ class TestAgentTasks(TacticalTestCase):
action = PendingAction.objects.get(agent__pk=agent64_111.pk) action = PendingAction.objects.get(agent__pk=agent64_111.pk)
self.assertEqual(action.action_type, "agentupdate") self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending") self.assertEqual(action.status, "pending")
self.assertEqual(action.details["url"], settings.DL_64)
self.assertEqual( self.assertEqual(
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe" action.details["url"],
"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
) )
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER) self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
self.assertEqual(action.details["version"], "1.3.0")
agent64 = baker.make_recipe( agent_64_130 = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)", operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.12", version="1.3.0",
) )
nats_cmd.return_value = "ok" nats_cmd.return_value = "ok"
r = agent_update(agent64.pk) r = agent_update(agent_64_130.pk)
self.assertEqual(r, "created") self.assertEqual(r, "created")
nats_cmd.assert_called_with( nats_cmd.assert_called_with(
{ {
@@ -806,6 +785,26 @@ class TestAgentTasks(TacticalTestCase):
wait=False, wait=False,
) )
agent64_old = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.2.1",
)
nats_cmd.return_value = "ok"
r = agent_update(agent64_old.pk)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
"version": "1.3.0",
"inno": "winagent-v1.3.0.exe",
},
},
wait=False,
)
""" @patch("agents.models.Agent.salt_api_async") """ @patch("agents.models.Agent.salt_api_async")
@patch("agents.tasks.sleep", return_value=None) @patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):

View File

@@ -30,6 +30,7 @@ from .serializers import (
AgentEditSerializer, AgentEditSerializer,
NoteSerializer, NoteSerializer,
NotesSerializer, NotesSerializer,
AgentOverdueActionSerializer,
) )
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
@@ -333,26 +334,12 @@ def by_site(request, sitepk):
@api_view(["POST"]) @api_view(["POST"])
def overdue_action(request): def overdue_action(request):
pk = request.data["pk"] agent = get_object_or_404(Agent, pk=request.data["pk"])
alert_type = request.data["alertType"] serializer = AgentOverdueActionSerializer(
action = request.data["action"] instance=agent, data=request.data, partial=True
agent = get_object_or_404(Agent, pk=pk) )
if alert_type == "email" and action == "enabled": serializer.is_valid(raise_exception=True)
agent.overdue_email_alert = True serializer.save()
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
)
return Response(agent.hostname) return Response(agent.hostname)
@@ -473,7 +460,7 @@ def install_agent(request):
f"GOARCH={goarch}", f"GOARCH={goarch}",
go_bin, go_bin,
"build", "build",
f"-ldflags=\"-X 'main.Inno={inno}'", f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'", f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'", f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'", f"-X 'main.Site={site_id}'",
@@ -825,7 +812,7 @@ def bulk(request):
elif request.data["target"] == "agents": elif request.data["target"] == "agents":
q = Agent.objects.filter(pk__in=request.data["agentPKs"]) q = Agent.objects.filter(pk__in=request.data["agentPKs"])
elif request.data["target"] == "all": elif request.data["target"] == "all":
q = Agent.objects.all() q = Agent.objects.only("pk", "monitoring_type")
else: else:
return notify_error("Something went wrong") return notify_error("Something went wrong")

View File

@@ -75,3 +75,12 @@ class TestAPIv3(TacticalTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)
def test_checkrunner_interval(self):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)

View File

@@ -6,6 +6,7 @@ urlpatterns = [
path("hello/", views.Hello.as_view()), path("hello/", views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()), path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/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>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()), path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
path("meshexe/", views.MeshExe.as_view()), path("meshexe/", views.MeshExe.as_view()),

View File

@@ -21,7 +21,7 @@ from autotasks.models import AutomatedTask
from accounts.models import User from accounts.models import User
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdatePolicy
from software.models import InstalledSoftware from software.models import InstalledSoftware
from checks.serializers import CheckRunnerGetSerializerV3 from checks.serializers import CheckRunnerGetSerializer
from agents.serializers import WinAgentSerializer from agents.serializers import WinAgentSerializer
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from winupdate.serializers import ApprovedUpdateSerializer from winupdate.serializers import ApprovedUpdateSerializer
@@ -232,7 +232,7 @@ class CheckRunner(APIView):
ret = { ret = {
"agent": agent.pk, "agent": agent.pk,
"check_interval": agent.check_interval, "check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV3(checks, many=True).data, "checks": CheckRunnerGetSerializer(checks, many=True).data,
} }
return Response(ret) return Response(ret)
@@ -245,6 +245,15 @@ class CheckRunner(APIView):
return Response(status) 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): class TaskRunner(APIView):
""" """
For the windows golang agent For the windows golang agent
@@ -323,6 +332,7 @@ class WinUpdater(APIView):
update.installed = True update.installed = True
update.save(update_fields=["result", "downloaded", "installed"]) update.save(update_fields=["result", "downloaded", "installed"])
agent.delete_superseded_updates()
return Response("ok") return Response("ok")
# agent calls this after it's finished installing all patches # agent calls this after it's finished installing all patches
@@ -348,6 +358,7 @@ class WinUpdater(APIView):
f"{agent.hostname} is rebooting after updates were installed." f"{agent.hostname} is rebooting after updates were installed."
) )
agent.delete_superseded_updates()
return Response("ok") return Response("ok")

View File

@@ -11,11 +11,15 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
policy = Policy.objects.get(pk=policypk) policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy: 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: 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: 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: else:
agents = policy.related_agents() agents = policy.related_agents()
@@ -84,11 +88,15 @@ def generate_agent_tasks_from_policies_task(policypk):
policy = Policy.objects.get(pk=policypk) policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy: 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: 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: 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: else:
agents = policy.related_agents() agents = policy.related_agents()

View File

@@ -413,11 +413,15 @@ class UpdatePatchPolicy(APIView):
agents = None agents = None
if "client" in request.data: 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: 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: else:
agents = Agent.objects.all() agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
for agent in agents: for agent in agents:
winupdatepolicy = agent.winupdatepolicy.get() winupdatepolicy = agent.winupdatepolicy.get()

View File

@@ -7,7 +7,7 @@ class Command(BaseCommand):
help = "Checks for orphaned tasks on all agents and removes them" help = "Checks for orphaned tasks on all agents and removes them"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
agents = Agent.objects.all() agents = Agent.objects.only("pk", "last_seen", "overdue_time")
online = [i for i in agents if i.status == "online"] online = [i for i in agents if i.status == "online"]
for agent in online: for agent in online:
remove_orphaned_win_tasks.delay(agent.pk) remove_orphaned_win_tasks.delay(agent.pk)

View File

@@ -445,42 +445,6 @@ class Check(BaseAuditModel):
return self.status 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 @staticmethod
def serialize(check): def serialize(check):
# serializes the check and returns json # serializes the check and returns json

View File

@@ -95,101 +95,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer): class CheckRunnerGetSerializer(serializers.ModelSerializer):
# for the windows agent
# only send data needed for agent to run a check # 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() assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True) script = ScriptCheckSerializer(read_only=True)

View File

@@ -2,9 +2,9 @@ from checks.models import CheckHistory
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer from .serializers import CheckSerializer
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from unittest.mock import patch
from model_bakery import baker from model_bakery import baker
from itertools import cycle
class TestCheckViews(TacticalTestCase): class TestCheckViews(TacticalTestCase):
@@ -184,6 +184,48 @@ class TestCheckViews(TacticalTestCase):
self.check_not_authenticated("patch", url_a) 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): def test_get_check_history(self):
# setup data # setup data
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from packaging import version as pyver
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Q from django.db.models import Q
@@ -168,8 +169,17 @@ def run_checks(request, pk):
if not agent.has_nats: if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater") return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
return Response(agent.hostname) 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() @api_view()

View File

@@ -223,7 +223,7 @@ class GenerateAgent(APIView):
f"GOARCH={goarch}", f"GOARCH={goarch}",
go_bin, go_bin,
"build", "build",
f"-ldflags=\"-X 'main.Inno={inno}'", f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'", f"-X 'main.Api={api}'",
f"-X 'main.Client={d.client.pk}'", f"-X 'main.Client={d.client.pk}'",
f"-X 'main.Site={d.site.pk}'", f"-X 'main.Site={d.site.pk}'",

View File

@@ -16,7 +16,7 @@ class Command(BaseCommand):
# 10-16-2020 changed the type of the agent's 'disks' model field # 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 # 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 # 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: for agent in agents:
if agent.disks is not None and isinstance(agent.disks, dict): if agent.disks is not None and isinstance(agent.disks, dict):
new = [] new = []

View File

@@ -105,7 +105,7 @@ def server_maintenance(request):
from agents.models import Agent from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks from autotasks.tasks import remove_orphaned_win_tasks
agents = Agent.objects.all() agents = Agent.objects.only("pk", "last_seen", "overdue_time")
online = [i for i in agents if i.status == "online"] online = [i for i in agents if i.status == "online"]
for agent in online: for agent in online:
remove_orphaned_win_tasks.delay(agent.pk) remove_orphaned_win_tasks.delay(agent.pk)

View File

@@ -140,7 +140,7 @@ def cancel_pending_action(request):
def debug_log(request, mode, hostname, order): def debug_log(request, mode, hostname, order):
log_file = settings.LOG_CONFIG["handlers"][0]["sink"] 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) agent_hostnames = AgentHostnameSerializer(agents, many=True)
switch_mode = { switch_mode = {

View File

@@ -176,6 +176,7 @@ class NatsWinUpdates(APIView):
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.") logger.info(f"{agent.hostname} is rebooting after updates were installed.")
agent.delete_superseded_updates()
return Response("ok") return Response("ok")
def patch(self, request): def patch(self, request):
@@ -199,6 +200,7 @@ class NatsWinUpdates(APIView):
u.result = "failed" u.result = "failed"
u.save(update_fields=["result"]) u.save(update_fields=["result"])
agent.delete_superseded_updates()
return Response("ok") return Response("ok")
def post(self, request): def post(self, request):
@@ -233,4 +235,5 @@ class NatsWinUpdates(APIView):
revision_number=update["revision_number"], revision_number=update["revision_number"],
).save() ).save()
agent.delete_superseded_updates()
return Response("ok") return Response("ok")

View File

@@ -1,3 +1,6 @@
black black
Werkzeug Werkzeug
django-extensions django-extensions
mkdocs
mkdocs-material
pymdown-extensions

View File

@@ -193,6 +193,6 @@
"submittedBy": "https://github.com/dinger1986", "submittedBy": "https://github.com/dinger1986",
"name": "TRMM Defender Exclusions", "name": "TRMM Defender Exclusions",
"description": "Windows Defender Exclusions for Tactical RMM", "description": "Windows Defender Exclusions for Tactical RMM",
"shell": "cmd" "shell": "powershell"
} }
] ]

View File

@@ -72,6 +72,7 @@ class Script(BaseAuditModel):
i.name = script["name"] i.name = script["name"]
i.description = script["description"] i.description = script["description"]
i.category = "Community" i.category = "Community"
i.shell = script["shell"]
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
script_bytes = ( script_bytes = (
@@ -80,7 +81,13 @@ class Script(BaseAuditModel):
i.code_base64 = base64.b64encode(script_bytes).decode("ascii") i.code_base64 = base64.b64encode(script_bytes).decode("ascii")
i.save( i.save(
update_fields=["name", "description", "category", "code_base64"] update_fields=[
"name",
"description",
"category",
"code_base64",
"shell",
]
) )
else: else:
print(f"Adding new community script: {script['name']}") print(f"Adding new community script: {script['name']}")

View File

@@ -15,16 +15,16 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User" AUTH_USER_MODEL = "accounts.User"
# latest release # latest release
TRMM_VERSION = "0.4.0" TRMM_VERSION = "0.4.2"
# bump this version everytime vue code is changed # bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser # to alert user they need to manually refresh their browser
APP_VER = "0.0.107" APP_VER = "0.0.109"
# https://github.com/wh1te909/rmmagent # https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.3.0" LATEST_AGENT_VER = "1.4.1"
MESH_VER = "0.7.49" MESH_VER = "0.7.54"
# for the update script, bump when need to recreate venv or npm install # for the update script, bump when need to recreate venv or npm install
PIP_VER = "7" PIP_VER = "7"

View File

@@ -19,8 +19,9 @@ logger.configure(**settings.LOG_CONFIG)
def auto_approve_updates_task(): def auto_approve_updates_task():
# scheduled task that checks and approves updates daily # scheduled task that checks and approves updates daily
agents = Agent.objects.all() agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
for agent in agents: for agent in agents:
agent.delete_superseded_updates()
try: try:
agent.approve_updates() agent.approve_updates()
except: except:
@@ -43,7 +44,7 @@ def auto_approve_updates_task():
@app.task @app.task
def check_agent_update_schedule_task(): def check_agent_update_schedule_task():
# scheduled task that installs updates on agents if enabled # scheduled task that installs updates on agents if enabled
agents = Agent.objects.all() agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
online = [ online = [
i i
for i in agents for i in agents
@@ -53,6 +54,7 @@ def check_agent_update_schedule_task():
] ]
for agent in online: for agent in online:
agent.delete_superseded_updates()
install = False install = False
patch_policy = agent.get_patch_policy() patch_policy = agent.get_patch_policy()
@@ -126,6 +128,7 @@ def bulk_install_updates_task(pks: List[int]) -> None:
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
for chunk in chunks: for chunk in chunks:
for agent in chunk: for agent in chunk:
agent.delete_superseded_updates()
nats_data = { nats_data = {
"func": "installwinupdates", "func": "installwinupdates",
"guids": agent.get_approved_update_guids(), "guids": agent.get_approved_update_guids(),
@@ -142,6 +145,7 @@ def bulk_check_for_updates_task(pks: List[int]) -> None:
chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) chunks = (agents[i : i + 40] for i in range(0, len(agents), 40))
for chunk in chunks: for chunk in chunks:
for agent in chunk: for agent in chunk:
agent.delete_superseded_updates()
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
time.sleep(0.05) time.sleep(0.05)
time.sleep(15) time.sleep(15)

View File

@@ -22,6 +22,7 @@ def get_win_updates(request, pk):
@api_view() @api_view()
def run_update_scan(request, pk): def run_update_scan(request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
agent.delete_superseded_updates()
if pyver.parse(agent.version) < pyver.parse("1.3.0"): if pyver.parse(agent.version) < pyver.parse("1.3.0"):
return notify_error("Requires agent version 1.3.0 or greater") return notify_error("Requires agent version 1.3.0 or greater")
@@ -32,6 +33,7 @@ def run_update_scan(request, pk):
@api_view() @api_view()
def install_updates(request, pk): def install_updates(request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
agent.delete_superseded_updates()
if pyver.parse(agent.version) < pyver.parse("1.3.0"): if pyver.parse(agent.version) < pyver.parse("1.3.0"):
return notify_error("Requires agent version 1.3.0 or greater") return notify_error("Requires agent version 1.3.0 or greater")

View File

@@ -1,12 +0,0 @@
pids
logs
node_modules
npm-debug.log
coverage/
run
dist
.DS_Store
.nyc_output
.basement
config.local.js
basement_dist

View File

@@ -1,41 +0,0 @@
const { description } = require('../package')
module.exports = {
base: '/tacticalrmm/',
title: 'Tactical RMM',
description: description,
head: [
['meta', { name: 'theme-color', content: '#3eaf7c' }],
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }]
],
themeConfig: {
repo: '',
editLinks: false,
docsDir: '',
editLinkText: '',
lastUpdated: false,
nav: [
{
text: 'Guide',
link: '/guide/',
}
],
sidebar: {
'/guide/': [
{
title: 'Guide',
collapsable: false,
children: [
'',
]
}
],
}
},
plugins: [
//'@vuepress/plugin-back-to-top',
//'@vuepress/plugin-medium-zoom',
]
}

View File

@@ -1,14 +0,0 @@
/**
* Client app enhancement file.
*
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
*/
export default ({
Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance
router, // the router instance for the app
siteData // site metadata
}) => {
// ...apply enhancements for the site.
}

View File

@@ -1,8 +0,0 @@
/**
* Custom Styles here.
*
* refhttps://v1.vuepress.vuejs.org/config/#index-styl
*/
.home .hero img
max-width 450px!important

View File

@@ -1,10 +0,0 @@
/**
* Custom palette here.
*
* refhttps://v1.vuepress.vuejs.org/zh/config/#palette-styl
*/
$accentColor = #3eaf7c
$textColor = #2c3e50
$borderColor = #eaecef
$codeBgColor = #282c34

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

BIN
docs/docs/images/onit.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

28
docs/docs/index.md Normal file
View File

@@ -0,0 +1,28 @@
# Tactical RMM Documentation
[![Build Status](https://dev.azure.com/dcparsi/Tactical%20RMM/_apis/build/status/wh1te909.tacticalrmm?branchName=develop)](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
[![Coverage Status](https://coveralls.io/repos/github/wh1te909/tacticalrmm/badge.png?branch=develop&kill_cache=1)](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django, Vue and Golang.
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
## [LIVE DEMO](https://rmm.xlawgaming.com/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
## Features
- Teamviewer-like remote desktop control
- Real-time remote shell
- Remote file browser (download and upload files)
- Remote command and script execution (batch, powershell and python scripts)
- Event log viewer
- Services management
- Windows patch management
- Automated checks with email/SMS alerting (cpu, disk, memory, services, scripts, event logs)
- Automated task runner (run scripts on a schedule)
- Remote software installation via chocolatey
- Software and hardware inventory

View File

@@ -0,0 +1,10 @@
.md-header {
background-color: black !important;
color: white !important;
}
.md-search__input {
background-color: white !important;
}
.md-search__icon[for=__search]{
color: initial;
}

View File

@@ -1,2 +0,0 @@
# Installation

View File

@@ -1,6 +0,0 @@
---
home: true
heroImage: https://v1.vuepress.vuejs.org/hero.png
actionText: Documentation →
actionLink: /guide/
---

33
docs/mkdocs.yml Normal file
View File

@@ -0,0 +1,33 @@
site_name: "Tactical RMM"
nav:
- Home: index.md
site_description: "A remote monitoring and management tool for Windows computers"
site_author: "wh1te909"
# Repository
repo_name: "wh1te909/tacticalrmm"
repo_url: "https://github.com/wh1te909/tacticalrmm"
edit_uri: ""
theme:
name: "material"
custom_dir: "theme"
logo: "images/onit.ico"
favicon: "images/favicon.ico"
language: "en"
palette:
primary: "white"
accent: "indigo"
extra_css:
- stylesheets/extra.css
extra:
social:
- icon: fontawesome/brands/github
link: "https://github.com/wh1te909/tacticalrmm"
markdown_extensions:
- pymdownx.inlinehilite
- admonition
- codehilite:
guess_lang: false
- toc:
permalink: true

10783
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
{
"name": "tacticalrmm",
"description": "A remote monitoring and management tool",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vuepress dev",
"build": "vuepress build"
},
"license": "MIT",
"devDependencies": {
"vuepress": "^1.5.3"
}
}

4
docs/theme/main.html vendored Normal file
View File

@@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block site_nav %}
{{ super() }}
{% endblock %}

70
docs/theme/partials/footer.html vendored Normal file
View File

@@ -0,0 +1,70 @@
{% import "partials/language.html" as lang with context %}
<!-- Application footer -->
<footer class="md-footer">
<!-- Link to previous and/or next page -->
{% if page.previous_page or page.next_page %}
<div class="md-footer-nav">
<nav class="md-footer-nav__inner md-grid">
<!-- Link to previous page -->
{% if page.previous_page %}
<a href="{{ page.previous_page.url | url }}" title="{{ page.previous_page.title }}"
class="md-flex md-footer-nav__link md-footer-nav__link--prev" rel="prev">
<div class="md-flex__cell md-flex__cell--shrink">
<i class="md-icon md-icon--arrow-back
md-footer-nav__button"></i>
</div>
<div class="md-flex__cell md-flex__cell--stretch
md-footer-nav__title">
<span class="md-flex__ellipsis">
<span class="md-footer-nav__direction">
{{ lang.t("footer.previous") }}
</span>
{{ page.previous_page.title }}
</span>
</div>
</a>
{% endif %}
<!-- Link to next page -->
{% if page.next_page %}
<a href="{{ page.next_page.url | url }}" title="{{ page.next_page.title }}"
class="md-flex md-footer-nav__link md-footer-nav__link--next" rel="next">
<div class="md-flex__cell md-flex__cell--stretch
md-footer-nav__title">
<span class="md-flex__ellipsis">
<span class="md-footer-nav__direction">
{{ lang.t("footer.next") }}
</span>
{{ page.next_page.title }}
</span>
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<i class="md-icon md-icon--arrow-forward
md-footer-nav__button"></i>
</div>
</a>
{% endif %}
</nav>
</div>
{% endif %}
<!-- Further information -->
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<!-- Copyright and theme information -->
<div class="md-footer-copyright">
{% if config.copyright %}
<div class="md-footer-copyright__highlight">
{{ config.copyright }}
</div>
{% endif %}
</div>
<!-- Social links -->
{% include "partials/social.html" %}
</div>
</div>
</footer>

View File

@@ -9,7 +9,7 @@ import (
"github.com/wh1te909/tacticalrmm/natsapi" "github.com/wh1te909/tacticalrmm/natsapi"
) )
var version = "1.0.1" var version = "1.0.2"
func main() { func main() {
ver := flag.Bool("version", false, "Prints version") ver := flag.Bool("version", false, "Prints version")
@@ -23,5 +23,5 @@ func main() {
return return
} }
api.Listen(*apiHost, *natsHost, *debug) api.Listen(*apiHost, *natsHost, version, *debug)
} }

View File

@@ -40,16 +40,15 @@ func getAPI(apihost, natshost string) (string, string, error) {
return "", "", errors.New("unable to parse api from nginx conf") return "", "", errors.New("unable to parse api from nginx conf")
} }
func Listen(apihost, natshost string, debug bool) { func Listen(apihost, natshost, version string, debug bool) {
api, natsurl, err := getAPI(apihost, natshost) api, natsurl, err := getAPI(apihost, natshost)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
if debug { log.Printf("Tactical Nats API Version %s\n", version)
log.Println("Api base url: ", api) log.Println("Api base url: ", api)
log.Println("Nats connection url: ", natsurl) log.Println("Nats connection url: ", natsurl)
}
rClient.SetHostURL(api) rClient.SetHostURL(api)
rClient.SetTimeout(30 * time.Second) rClient.SetTimeout(30 * time.Second)

Binary file not shown.

View File

@@ -229,8 +229,8 @@ export default {
.then(response => { .then(response => {
this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`)); this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`));
}) })
.catch(error => { .catch(e => {
this.$q.notify(notifyErrorConfig(`An Error occured while deleting user ${data.username}`)); this.$q.notify(notifyErrorConfig(e.response.data));
}); });
}); });
}, },
@@ -295,8 +295,8 @@ export default {
.then(response => { .then(response => {
this.$q.notify(notifySuccessConfig(response.data, 4000)); this.$q.notify(notifySuccessConfig(response.data, 4000));
}) })
.catch(error => { .catch(e => {
this.$q.notify(notifyErrorConfig("An Error occured while resetting key")); this.$q.notify(notifyErrorConfig(e.response.data));
}); });
}); });
}, },

View File

@@ -317,7 +317,7 @@
<q-tooltip>Reboot required</q-tooltip> <q-tooltip>Reboot required</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
<q-td key="lastseen" :props="props">{{ formatDate(props.row.last_seen) }}</q-td> <q-td key="lastseen" :props="props">{{ unixToString(props.row.last_seen) }}</q-td>
<q-td key="boottime" :props="props">{{ bootTime(props.row.boot_time) }}</q-td> <q-td key="boottime" :props="props">{{ bootTime(props.row.boot_time) }}</q-td>
</q-tr> </q-tr>
</template> </template>
@@ -363,7 +363,7 @@ import axios from "axios";
import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins"; import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
import { openURL } from "quasar"; import { date } from "quasar";
import EditAgent from "@/components/modals/agents/EditAgent"; import EditAgent from "@/components/modals/agents/EditAgent";
import RebootLater from "@/components/modals/agents/RebootLater"; import RebootLater from "@/components/modals/agents/RebootLater";
import PendingActions from "@/components/modals/logs/PendingActions"; import PendingActions from "@/components/modals/logs/PendingActions";
@@ -440,9 +440,10 @@ export default {
if (availability === "online" && row.status !== "online") return false; if (availability === "online" && row.status !== "online") return false;
else if (availability === "offline" && row.status !== "overdue") return false; else if (availability === "offline" && row.status !== "overdue") return false;
else if (availability === "expired") { else if (availability === "expired") {
const nowPlus30Days = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); let now = new Date();
const unixtime = Date.parse(row.last_seen); let lastSeen = new Date(row.last_seen * 1000);
if (unixtime > nowPlus30Days) return false; let diff = date.getDateDiff(now, lastSeen, "days");
if (diff < 30) return false;
} }
} }
@@ -540,10 +541,17 @@ export default {
window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826"); window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826");
}, },
runChecks(pk) { runChecks(pk) {
axios this.$q.loading.show();
this.$axios
.get(`/checks/runchecks/${pk}/`) .get(`/checks/runchecks/${pk}/`)
.then(r => this.notifySuccess(`Checks will now be re-run on ${r.data}`)) .then(r => {
.catch(e => this.notifyError(e.response.data)); this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
});
}, },
removeAgent(pk, name) { removeAgent(pk, name) {
this.$q this.$q
@@ -636,14 +644,14 @@ export default {
this.$store.dispatch("loadNotes", pk); this.$store.dispatch("loadNotes", pk);
}, },
overdueAlert(category, pk, alert_action) { overdueAlert(category, pk, alert_action) {
const db_field = category === "email" ? "overdue_email_alert" : "overdue_text_alert";
const action = alert_action ? "enabled" : "disabled"; const action = alert_action ? "enabled" : "disabled";
const data = { const data = {
pk: pk, pk: pk,
alertType: category, [db_field]: alert_action,
action: action,
}; };
const alertColor = alert_action ? "positive" : "warning"; const alertColor = alert_action ? "positive" : "warning";
axios this.$axios
.post("/agents/overdueaction/", data) .post("/agents/overdueaction/", data)
.then(r => { .then(r => {
this.$q.notify({ this.$q.notify({
@@ -652,7 +660,7 @@ export default {
message: `Overdue ${category} alerts ${action} on ${r.data}`, message: `Overdue ${category} alerts ${action} on ${r.data}`,
}); });
}) })
.catch(e => this.notifyError(e.response.data.error)); .catch(() => this.notifyError("Something went wrong"));
}, },
agentClass(status) { agentClass(status) {
if (status === "offline") { if (status === "offline") {

View File

@@ -298,7 +298,6 @@
</template> </template>
<script> <script>
import axios from "axios";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import { mapState } from "vuex"; import { mapState } from "vuex";
import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy"; import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy";
@@ -329,7 +328,7 @@ export default {
}, },
methods: { methods: {
getCoreSettings() { getCoreSettings() {
axios.get("/core/getcoresettings/").then(r => { this.$axios.get("/core/getcoresettings/").then(r => {
this.settings = r.data; this.settings = r.data;
this.allTimezones = Object.freeze(r.data.all_timezones); this.allTimezones = Object.freeze(r.data.all_timezones);
this.ready = true; this.ready = true;
@@ -388,7 +387,8 @@ export default {
}, },
editSettings() { editSettings() {
this.$q.loading.show(); this.$q.loading.show();
axios delete this.settings.all_timezones;
this.$axios
.patch("/core/editsettings/", this.settings) .patch("/core/editsettings/", this.settings)
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();

View File

@@ -1,4 +1,4 @@
import { Notify } from "quasar"; import { Notify, date } from "quasar";
export function notifySuccessConfig(msg, timeout = 2000) { export function notifySuccessConfig(msg, timeout = 2000) {
return { return {
@@ -95,6 +95,10 @@ export default {
return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted
}, },
unixToString(timestamp) {
let t = new Date(timestamp * 1000)
return date.formatDate(t, 'MMM-D-YYYY - HH:mm')
},
formatClientOptions(clients) { formatClientOptions(clients) {
return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites })) return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites }))
}, },

View File

@@ -661,7 +661,7 @@ export default {
this.poll = setInterval(() => { this.poll = setInterval(() => {
this.$store.dispatch("checkVer"); this.$store.dispatch("checkVer");
this.getAgentCounts(); this.getAgentCounts();
this.getDashInfo(); this.getDashInfo(false);
}, 60 * 5 * 1000); }, 60 * 5 * 1000);
}, },
setSplitter(val) { setSplitter(val) {