Compare commits

...

24 Commits

Author SHA1 Message Date
wh1te909
763877541a Release 0.2.14 2020-12-12 01:59:47 +00:00
wh1te909
1fad7d72a2 fix for special chars in computer hostname closes #201 2020-12-12 01:59:10 +00:00
wh1te909
51ea2ea879 Release 0.2.13 2020-12-11 20:48:11 +00:00
wh1te909
d77a478bf0 agent 1.1.8 2020-12-11 20:47:54 +00:00
wh1te909
e413c0264a Release 0.2.12 2020-12-11 07:28:27 +00:00
wh1te909
f88e7f898c bump versions 2020-12-11 07:27:42 +00:00
wh1te909
d07bd4a6db add optional silent flag to installer 2020-12-11 07:25:42 +00:00
wh1te909
fb34c099d5 Release 0.2.11 2020-12-10 19:13:24 +00:00
wh1te909
1d2ee56a15 bump versions 2020-12-10 19:12:30 +00:00
wh1te909
86665f7f09 change update task for agent 1.1.6 2020-12-10 19:08:29 +00:00
wh1te909
0d2b4af986 Release 0.2.10 2020-12-10 10:34:40 +00:00
wh1te909
dc2b2eeb9f bump versions 2020-12-10 10:33:44 +00:00
wh1te909
e5dbb66d53 cleanup agent update func 2020-12-10 10:31:58 +00:00
wh1te909
3474b1c471 fix failing checks alert 2020-12-10 00:01:54 +00:00
wh1te909
3886de5b7c add postgres vacuum 2020-12-10 00:00:02 +00:00
wh1te909
2b3cec06b3 Release 0.2.9 2020-12-09 05:07:11 +00:00
wh1te909
8536754d14 bump version for new agent 2020-12-09 05:06:19 +00:00
wh1te909
1f36235801 fix wording 2020-12-09 05:04:25 +00:00
wh1te909
a4194b14f9 Release 0.2.8 2020-12-09 00:50:48 +00:00
wh1te909
2dcc629d9d bump versions 2020-12-09 00:31:33 +00:00
wh1te909
98ddadc6bc add sync task 2020-12-08 23:02:05 +00:00
wh1te909
f6e47b7383 remove extra services view 2020-12-08 20:09:09 +00:00
wh1te909
f073ddc906 Release 0.2.7 2020-12-07 09:50:37 +00:00
wh1te909
3e00631925 cleanup older pending action agent updates if one exists with an older agent version 2020-12-07 09:50:15 +00:00
16 changed files with 220 additions and 188 deletions

View File

@@ -164,13 +164,11 @@ class Agent(BaseAuditModel):
elif i.status == "failing":
failing += 1
has_failing_checks = True if failing > 0 else False
ret = {
"total": total,
"passing": passing,
"failing": failing,
"has_failing_checks": has_failing_checks,
"has_failing_checks": failing > 0,
}
return ret

View File

@@ -1,8 +1,10 @@
import asyncio
from loguru import logger
from time import sleep
import random
import requests
from packaging import version as pyver
from typing import List
from django.conf import settings
@@ -13,137 +15,104 @@ from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG)
OLD_64_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2.exe"
OLD_32_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2-x86.exe"
def agent_update(pk: int) -> str:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
return "noarch"
# force an update to 1.1.5 since 1.1.6 needs agent to be on 1.1.5 first
if pyver.parse(agent.version) < pyver.parse("1.1.5"):
version = "1.1.5"
if agent.arch == "64":
url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5.exe"
inno = "winagent-v1.1.5.exe"
elif agent.arch == "32":
url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5-x86.exe"
inno = "winagent-v1.1.5-x86.exe"
else:
return "nover"
else:
version = settings.LATEST_AGENT_VER
url = agent.winagent_dl
inno = agent.win_inno_exe
if agent.has_nats:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
action = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
if pyver.parse(action.details["version"]) < pyver.parse(version):
action.delete()
else:
return "pending"
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
return "created"
# TODO
# Salt is deprecated, remove this once salt is gone
else:
agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": inno,
"url": url,
},
)
return "salt"
@app.task
def send_agent_update_task(pks, version):
assert isinstance(pks, list)
def send_agent_update_task(pks: List[int], version: str) -> None:
q = Agent.objects.filter(pk__in=pks)
agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)]
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
]
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
inno = (
"winagent-v0.11.2.exe"
if agent.arch == "64"
else "winagent-v0.11.2-x86.exe"
)
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
if agent.has_nats:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
continue
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": agent.winagent_dl,
"version": settings.LATEST_AGENT_VER,
"inno": agent.win_inno_exe,
},
)
# TODO
# Salt is deprecated, remove this once salt is gone
else:
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": inno,
"url": url,
},
)
sleep(5)
for pk in agents:
agent_update(pk)
@app.task
def auto_self_agent_update_task():
def auto_self_agent_update_task() -> None:
core = CoreSettings.objects.first()
if not core.agent_auto_update:
logger.info("Agent auto update is disabled. Skipping.")
return
q = Agent.objects.only("pk", "version")
agents = [
pks: List[int] = [
i.pk
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
for pk in pks:
agent_update(pk)
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
inno = (
"winagent-v0.11.2.exe"
if agent.arch == "64"
else "winagent-v0.11.2-x86.exe"
)
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
if agent.has_nats:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
continue
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": agent.winagent_dl,
"version": settings.LATEST_AGENT_VER,
"inno": agent.win_inno_exe,
},
)
# TODO
# Salt is deprecated, remove this once salt is gone
else:
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": inno,
"url": url,
},
)
sleep(5)
@app.task
def sync_sysinfo_task():
agents = Agent.objects.all()
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online"
]
for agent in online:
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
@app.task

View File

@@ -5,19 +5,20 @@ from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
from django.test import TestCase, override_settings
from django.conf import settings
from django.utils import timezone as djangotime
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .tasks import (
agent_recovery_sms_task,
auto_self_agent_update_task,
sync_salt_modules_task,
batch_sync_modules_task,
OLD_64_PY_AGENT,
OLD_32_PY_AGENT,
)
from winupdate.models import WinUpdatePolicy
@@ -786,6 +787,70 @@ class TestAgentTasks(TacticalTestCase):
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_async")
def test_agent_update(self, salt_api_async):
from agents.tasks import agent_update
agent_noarch = baker.make_recipe(
"agents.agent",
operating_system="Error getting OS",
version="1.1.0",
)
r = agent_update(agent_noarch.pk)
self.assertEqual(r, "noarch")
self.assertEqual(
PendingAction.objects.filter(
agent=agent_noarch, action_type="agentupdate"
).count(),
0,
)
agent64_nats = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.0",
)
r = agent_update(agent64_nats.pk)
self.assertEqual(r, "created")
action = PendingAction.objects.get(agent__pk=agent64_nats.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(action.details["url"], settings.DL_64)
self.assertEqual(
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
)
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
agent64_salt = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0",
)
salt_api_async.return_value = True
r = agent_update(agent64_salt.pk)
self.assertEqual(r, "salt")
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
"url": settings.DL_64,
},
)
salt_api_async.reset_mock()
agent32_nats = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.23964)",
version="1.1.0",
)
agent32_salt = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.23964)",
version="1.0.0",
)
""" @patch("agents.models.Agent.salt_api_async")
@patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
# test 64bit golang agent
@@ -888,4 +953,4 @@ class TestAgentTasks(TacticalTestCase):
"url": OLD_32_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.assertEqual(ret.status, "SUCCESS") """

View File

@@ -38,7 +38,6 @@ class Client(BaseAuditModel):
@property
def has_failing_checks(self):
agents = (
Agent.objects.only(
"pk",
@@ -50,14 +49,17 @@ class Client(BaseAuditModel):
.filter(site__client=self)
.prefetch_related("agentchecks")
)
failing = 0
for agent in agents:
if agent.checks["has_failing_checks"]:
return True
failing += 1
if agent.overdue_email_alert or agent.overdue_text_alert:
return agent.status == "overdue"
if agent.status == "overdue":
failing += 1
return False
return failing > 0
@staticmethod
def serialize(client):
@@ -98,7 +100,6 @@ class Site(BaseAuditModel):
@property
def has_failing_checks(self):
agents = (
Agent.objects.only(
"pk",
@@ -110,14 +111,17 @@ class Site(BaseAuditModel):
.filter(site=self)
.prefetch_related("agentchecks")
)
failing = 0
for agent in agents:
if agent.checks["has_failing_checks"]:
return True
failing += 1
if agent.overdue_email_alert or agent.overdue_text_alert:
return agent.status == "overdue"
if agent.status == "overdue":
failing += 1
return False
return failing > 0
@staticmethod
def serialize(site):

View File

@@ -58,6 +58,7 @@ func main() {
debugLog := flag.String("log", "", "Verbose output")
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
noSalt := flag.Bool("nosalt", false, "Does not install salt")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
flag.Parse()
@@ -78,7 +79,11 @@ func main() {
}
if debug {
cmdArgs = append(cmdArgs, "--log", "DEBUG")
cmdArgs = append(cmdArgs, "-log", "debug")
}
if *silent {
cmdArgs = append(cmdArgs, "-silent")
}
if *noSalt {
@@ -86,27 +91,27 @@ func main() {
}
if len(strings.TrimSpace(*localMesh)) != 0 {
cmdArgs = append(cmdArgs, "--local-mesh", *localMesh)
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
}
if len(strings.TrimSpace(*cert)) != 0 {
cmdArgs = append(cmdArgs, "--cert", *cert)
cmdArgs = append(cmdArgs, "-cert", *cert)
}
if len(strings.TrimSpace(*timeout)) != 0 {
cmdArgs = append(cmdArgs, "--timeout", *timeout)
cmdArgs = append(cmdArgs, "-timeout", *timeout)
}
if Rdp == "1" {
cmdArgs = append(cmdArgs, "--rdp")
cmdArgs = append(cmdArgs, "-rdp")
}
if Ping == "1" {
cmdArgs = append(cmdArgs, "--ping")
cmdArgs = append(cmdArgs, "-ping")
}
if Power == "1" {
cmdArgs = append(cmdArgs, "--power")
cmdArgs = append(cmdArgs, "-power")
}
if debug {

View File

@@ -9,21 +9,6 @@ class TestServiceViews(TacticalTestCase):
def setUp(self):
self.authenticate()
def test_get_services(self):
# test a call where agent doesn't exist
resp = self.client.get("/services/500/services/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent_with_services")
url = f"/services/{agent.pk}/services/"
serializer = ServicesSerializer(agent)
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(serializer.data, resp.data)
self.check_not_authenticated("get", url)
def test_default_services(self):
url = "/services/defaultservices/"
resp = self.client.get(url, format="json")
@@ -33,13 +18,13 @@ class TestServiceViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
def test_get_refreshed_services(self, nats_cmd):
def test_get_services(self, nats_cmd):
# test a call where agent doesn't exist
resp = self.client.get("/services/500/refreshedservices/", format="json")
resp = self.client.get("/services/500/services/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent_with_services")
url = f"/services/{agent.pk}/refreshedservices/"
url = f"/services/{agent.pk}/services/"
nats_return = [
{

View File

@@ -4,7 +4,6 @@ from . import views
urlpatterns = [
path("<int:pk>/services/", views.get_services),
path("defaultservices/", views.default_services),
path("<int:pk>/refreshedservices/", views.get_refreshed_services),
path("serviceaction/", views.service_action),
path("<int:pk>/<svcname>/servicedetail/", views.service_detail),
path("editservice/", views.edit_service),

View File

@@ -19,17 +19,6 @@ logger.configure(**settings.LOG_CONFIG)
@api_view()
def get_services(request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(ServicesSerializer(agent).data)
@api_view()
def default_services(request):
return Response(Check.load_default_services())
@api_view()
def get_refreshed_services(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
@@ -43,6 +32,11 @@ def get_refreshed_services(request, pk):
return Response(ServicesSerializer(agent).data)
@api_view()
def default_services(request):
return Response(Check.load_default_services())
@api_view(["POST"])
def service_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])

View File

@@ -41,6 +41,10 @@ app.conf.beat_schedule = {
"task": "agents.tasks.auto_self_agent_update_task",
"schedule": crontab(minute=35, hour="*"),
},
"agents-sync": {
"task": "agents.tasks.sync_sysinfo_task",
"schedule": crontab(minute=55, hour="*"),
},
}

View File

@@ -23,7 +23,7 @@ EXCLUDE_PATHS = (
"/logs/downloadlog",
)
ENDS_WITH = "/refreshedservices/"
ENDS_WITH = "/services/"
class AuditMiddleware:

View File

@@ -15,19 +15,19 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.2.6"
TRMM_VERSION = "0.2.14"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.96"
APP_VER = "0.0.99"
# https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.1.0"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.1.2"
LATEST_AGENT_VER = "1.1.9"
MESH_VER = "0.7.10"
MESH_VER = "0.7.14"
SALT_MASTER_VER = "3002.2"

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="4"
SCRIPT_VERSION="5"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
GREEN='\033[0;32m'
@@ -50,6 +50,11 @@ if [ -d /meshcentral/meshcentral-coredumps ]; then
rm -f /meshcentral/meshcentral-coredumps/*
fi
printf >&2 "${GREEN}Running postgres vacuum${NC}\n"
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog"
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction"
sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage"
dt_now=$(date '+%Y_%m_%d__%H_%M_%S')
tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX)
sysd="/etc/systemd/system"

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="99"
SCRIPT_VERSION="100"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh'
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
YELLOW='\033[1;33m'
@@ -177,6 +177,11 @@ sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/
sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo
sudo chmod +x /usr/local/bin/goversioninfo
printf >&2 "${GREEN}Running postgres vacuum${NC}\n"
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog"
sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction"
sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage"
if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then
rm -rf /rmm/api/env
cd /rmm/api

View File

@@ -13,7 +13,7 @@
hide-bottom
>
<template v-slot:top>
<q-btn dense flat push @click="refreshServices" icon="refresh" />
<q-btn dense flat push @click="getServices" icon="refresh" />
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
@@ -242,7 +242,7 @@ export default {
.then(r => {
this.serviceDetailVisible = false;
this.serviceDetailsModal = false;
this.refreshServices();
this.getServices();
this.notifySuccess(`Service ${name} was edited!`);
})
.catch(e => {
@@ -303,7 +303,7 @@ export default {
this.$axios
.post("/services/serviceaction/", data)
.then(r => {
this.refreshServices();
this.getServices();
this.serviceDetailsModal = false;
this.notifySuccess(`Service ${fullname} was ${status}!`);
})
@@ -313,19 +313,9 @@ export default {
});
},
getServices() {
this.$q.loading.show({ message: "Loading services..." });
this.$axios
.get(`/services/${this.pk}/services/`)
.then(r => {
this.servicesData = [r.data][0].services;
})
.catch(e => {
this.notifyError(e.response.data);
});
},
refreshServices() {
this.$q.loading.show({ message: "Reloading services..." });
this.$axios
.get(`/services/${this.pk}/refreshedservices/`)
.then(r => {
this.servicesData = [r.data][0].services;
this.$q.loading.hide();

View File

@@ -26,19 +26,25 @@
>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--log DEBUG</code>
<code>-log debug</code>
</q-badge>
<span>To enable verbose output during the install</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--nosalt</code>
<code>-silent</code>
</q-badge>
<span>Do not popup any message boxes during install</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>-nosalt</code>
</q-badge>
<span> Do not install salt during agent install. </span>
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--local-mesh "C:\\&lt;some folder or path&gt;\\meshagent.exe"</code>
<code>-local-mesh "C:\\&lt;some folder or path&gt;\\meshagent.exe"</code>
</q-badge>
<span>
To skip downloading the Mesh Agent during the install. Download it
@@ -49,7 +55,7 @@
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--cert "C:\\&lt;some folder or path&gt;\\ca.pem"</code>
<code>-cert "C:\\&lt;some folder or path&gt;\\ca.pem"</code>
</q-badge>
<span> To use a domain CA </span>
</div>

View File

@@ -27,10 +27,13 @@
<p>Fix issues with the Tactical Checkrunner windows service which handles running all checks.</p>
</q-card-section>
<q-card-section v-show="mode === 'salt'">
<p>Fix issues with the salt-minion which handles windows updates, chocolatey and scheduled tasks.</p>
<p>Fix issues with the salt-minion which handles windows updates and chocolatey.</p>
</q-card-section>
<q-card-section v-show="mode === 'rpc'">
<p>Fix issues with the Tactical RPC service which handles most of the agent's realtime functions.</p>
<p>
Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled
tasks.
</p>
</q-card-section>
<q-card-section v-show="mode === 'command'">
<p>Run a shell command on the agent.</p>