Compare commits

..

53 Commits

Author SHA1 Message Date
wh1te909
ff69bed394 Release 0.2.5 2020-12-02 11:06:55 +00:00
wh1te909
d6e8c5146f bump version 2020-12-02 11:06:34 +00:00
wh1te909
9a04cf99d7 fix pending actions ui 2020-12-02 11:05:29 +00:00
wh1te909
86e7c11e71 fix mesh nginx 2020-12-02 10:40:20 +00:00
wh1te909
361cc08faa Release 0.2.4 2020-12-02 05:45:55 +00:00
wh1te909
70dc771052 bump rmm and agent ver 2020-12-02 05:35:13 +00:00
wh1te909
c14873a799 update optional args 2020-12-02 05:33:35 +00:00
wh1te909
bba5abd74b bump script vers 2020-12-02 05:16:16 +00:00
wh1te909
a224e79c1f bump mesh and vue 2020-12-02 04:51:05 +00:00
wh1te909
c305d98186 remove old code 2020-12-02 04:14:35 +00:00
wh1te909
7c5a473e71 add flag to skip salt during agent install 2020-12-02 04:00:36 +00:00
wh1te909
5e0f5d1eed check for old installers 2020-12-02 03:23:16 +00:00
wh1te909
238b269bc4 remove update salt task 2020-12-02 03:22:19 +00:00
Josh
0ad121b9d2 fix tests attempt 2 2020-12-01 16:46:38 +00:00
Josh
7088acd9fd fix tests and remove travis config 2020-12-01 16:41:59 +00:00
Josh
e0a900d4b6 test for rm_orphaned_task in core maintenance 2020-12-01 16:35:34 +00:00
Josh
a0fe2f0c7d fix tests 2020-12-01 16:11:03 +00:00
Josh
d5b9bc2f26 get cert file locations from settings in docker build 2020-12-01 16:10:49 +00:00
Josh
584254e6ca fix/add tests 2020-12-01 15:55:26 +00:00
wh1te909
a2963ed7bb reload table when pending action changed 2020-12-01 07:01:50 +00:00
wh1te909
2a3c2e133d fix wording 2020-12-01 06:43:52 +00:00
wh1te909
3e7dcb2755 don't hide refresh when sw list empty 2020-12-01 06:27:34 +00:00
wh1te909
faeec00b39 remove more tasks now handled by the agent 2020-12-01 06:16:09 +00:00
wh1te909
eeed81392f add rm orphaned tasks to maintenance tab 2020-12-01 05:55:27 +00:00
wh1te909
95dce9e992 check for supported agent 2020-12-01 05:52:32 +00:00
wh1te909
502bd2a191 patch nats 2020-12-01 05:16:47 +00:00
wh1te909
17ac92a9d0 remove dead code 2020-12-01 05:16:37 +00:00
wh1te909
ba028cde0c remove old api app 2020-12-01 05:00:13 +00:00
wh1te909
6e751e7a9b remove bg task that's handled by the agent now 2020-12-01 04:51:51 +00:00
wh1te909
948b56d0e6 add a ghetto check for non standard cert 2020-12-01 04:47:09 +00:00
wh1te909
4bf2dc9ece don't create unnecessary outage records 2020-12-01 04:44:38 +00:00
Josh
125823f8ab add server maintenance to tools menu 2020-12-01 03:44:58 +00:00
Josh
24d33397e9 add virtual scroll to audit log table 2020-12-01 02:17:20 +00:00
Josh
2c553825f4 add server-side pagination for audit logging 2020-12-01 02:01:10 +00:00
wh1te909
198c485e9a reduce threads 2020-11-30 21:51:25 +00:00
wh1te909
0138505507 reduce threads 2020-11-30 21:49:47 +00:00
wh1te909
5d50dcc600 add api endpoint for software 2020-11-30 21:45:12 +00:00
wh1te909
7bdd8c4626 add some type hints 2020-11-30 10:28:25 +00:00
wh1te909
fc82c35f0c finish moving schedtasks to nats 2020-11-30 08:18:47 +00:00
wh1te909
426ebad300 start moving schedtasks to nats wh1te909/rmmagent@0cde11a067 2020-11-29 23:40:29 +00:00
sadnub
1afe61c593 fix docker-compose.yml 2020-11-29 14:24:32 -05:00
wh1te909
c20751829b create migration for schedtask weekdays 2020-11-29 10:37:46 +00:00
Tragic Bronson
a3b8ee8392 Merge pull request #194 from sadnub/develop
Get mesh version for settings.py
2020-11-28 21:02:58 -08:00
Josh
156c0fe7f6 add dockerignore and get MESH_VER from settings.py 2020-11-29 04:47:34 +00:00
wh1te909
216f7a38cf support mesh > 0.6.84 wh1te909/rmmagent@85aab2facf 2020-11-29 04:15:57 +00:00
Tragic Bronson
fd04dc10d4 Merge pull request #193 from sadnub/feature-uichanges
Some fixes
2020-11-28 19:48:41 -08:00
Josh
d39bdce926 add install agent to site context menu 2020-11-29 03:30:31 +00:00
Josh
c6e01245b0 fix disabled prop on edit agent patch policy and agent checks tab 2020-11-29 02:56:35 +00:00
Josh
c168ee7ba4 bump app version and mesh version 2020-11-29 02:44:29 +00:00
Josh
7575253000 regenerate policies and tasks on site/client change on agent 2020-11-29 02:35:30 +00:00
Josh
c28c1efbb1 Add pending actions to agent table and filter 2020-11-29 02:13:50 +00:00
sadnub
e6aa2c3b78 Delete docker-build-publish.yml 2020-11-28 09:47:41 -05:00
sadnub
ab7c481f83 Create docker-build-push.yml 2020-11-28 09:47:27 -05:00
69 changed files with 1050 additions and 1285 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.git
.cache
**/*.env
**/env
**/node_modules

View File

@@ -1,7 +1,8 @@
name: Publish Tactical Docker Images name: Publish Tactical Docker Images
on: on:
release: push:
types: [published] tags:
- "v*.*.*"
jobs: jobs:
docker: docker:
name: Build and Push Docker Images name: Build and Push Docker Images

View File

@@ -2,7 +2,7 @@
"python.pythonPath": "api/tacticalrmm/env/bin/python", "python.pythonPath": "api/tacticalrmm/env/bin/python",
"python.languageServer": "Pylance", "python.languageServer": "Pylance",
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
"api/tacticalrmm" "api/tacticalrmm",
], ],
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic",
"python.formatting.provider": "black", "python.formatting.provider": "black",

View File

@@ -26,7 +26,7 @@ def get_wmi_data():
agent = Recipe( agent = Recipe(
Agent, Agent,
hostname="DESKTOP-TEST123", hostname="DESKTOP-TEST123",
version="1.1.0", version="1.1.1",
monitoring_type=cycle(["workstation", "server"]), monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"), salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123", agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",

View File

@@ -1,5 +1,4 @@
import requests import requests
import datetime as dt
import time import time
import base64 import base64
from Crypto.Cipher import AES from Crypto.Cipher import AES
@@ -8,9 +7,7 @@ from Crypto.Hash import SHA3_384
from Crypto.Util.Padding import pad from Crypto.Util.Padding import pad
import validators import validators
import msgpack import msgpack
import random
import re import re
import string
from collections import Counter from collections import Counter
from loguru import logger from loguru import logger
from packaging import version as pyver from packaging import version as pyver
@@ -89,6 +86,10 @@ class Agent(BaseAuditModel):
def has_nats(self): def has_nats(self):
return pyver.parse(self.version) >= pyver.parse("1.1.0") 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 @property
def timezone(self): def timezone(self):
# return the default timezone unless the timezone is explicity set per agent # return the default timezone unless the timezone is explicity set per agent
@@ -573,61 +574,6 @@ class Agent(BaseAuditModel):
return resp return resp
def schedule_reboot(self, obj):
start_date = dt.datetime.strftime(obj, "%Y-%m-%d")
start_time = dt.datetime.strftime(obj, "%H:%M")
# let windows task scheduler automatically delete the task after it runs
end_obj = obj + dt.timedelta(minutes=15)
end_date = dt.datetime.strftime(end_obj, "%Y-%m-%d")
end_time = dt.datetime.strftime(end_obj, "%H:%M")
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
r = self.salt_api_cmd(
timeout=15,
func="task.create_task",
arg=[
f"name={task_name}",
"force=True",
"action_type=Execute",
'cmd="C:\\Windows\\System32\\shutdown.exe"',
'arguments="/r /t 5 /f"',
"trigger_type=Once",
f'start_date="{start_date}"',
f'start_time="{start_time}"',
f'end_date="{end_date}"',
f'end_time="{end_time}"',
"ac_only=False",
"stop_if_on_batteries=False",
"delete_after=Immediately",
],
)
if r == "error" or (isinstance(r, bool) and not r):
return "failed"
elif r == "timeout":
return "timeout"
elif isinstance(r, bool) and r:
from logs.models import PendingAction
details = {
"taskname": task_name,
"time": str(obj),
}
PendingAction(agent=self, action_type="schedreboot", details=details).save()
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
return {"msg": {"time": nice_time, "agent": self.hostname}}
else:
return "failed"
def not_supported(self, version_added):
return pyver.parse(self.version) < pyver.parse(version_added)
def delete_superseded_updates(self): def delete_superseded_updates(self):
try: try:
pks = [] # list of pks to delete pks = [] # list of pks to delete

View File

@@ -36,12 +36,16 @@ class AgentSerializer(serializers.ModelSerializer):
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()
status = serializers.ReadOnlyField() status = serializers.ReadOnlyField()
checks = serializers.ReadOnlyField() checks = serializers.ReadOnlyField()
last_seen = serializers.SerializerMethodField() last_seen = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name") client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name") site_name = serializers.ReadOnlyField(source="site.name")
def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count()
def get_last_seen(self, obj): def get_last_seen(self, obj):
if obj.time_zone is not None: if obj.time_zone is not None:
agent_tz = pytz.timezone(obj.time_zone) agent_tz = pytz.timezone(obj.time_zone)
@@ -62,6 +66,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
"description", "description",
"needs_reboot", "needs_reboot",
"patches_pending", "patches_pending",
"pending_actions",
"status", "status",
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",

View File

@@ -1,14 +1,11 @@
import asyncio
from loguru import logger from loguru import logger
from time import sleep from time import sleep
import random import random
import requests import requests
from packaging import version as pyver from packaging import version as pyver
from django.conf import settings from django.conf import settings
from tacticalrmm.celery import app from tacticalrmm.celery import app
from agents.models import Agent, AgentOutage from agents.models import Agent, AgentOutage
from core.models import CoreSettings from core.models import CoreSettings
@@ -52,9 +49,6 @@ def send_agent_update_task(pks, version):
else: else:
url = agent.winagent_dl url = agent.winagent_dl
inno = agent.win_inno_exe inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
if agent.has_nats: if agent.has_nats:
if agent.pendingactions.filter( if agent.pendingactions.filter(
@@ -81,7 +75,7 @@ def send_agent_update_task(pks, version):
"url": url, "url": url,
}, },
) )
sleep(10) sleep(5)
@app.task @app.task
@@ -97,7 +91,6 @@ def auto_self_agent_update_task():
for i in q for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
] ]
logger.info(f"Updating {len(agents)}")
chunks = (agents[i : i + 30] for i in range(0, len(agents), 30)) chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
@@ -124,9 +117,6 @@ def auto_self_agent_update_task():
else: else:
url = agent.winagent_dl url = agent.winagent_dl
inno = agent.win_inno_exe inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
if agent.has_nats: if agent.has_nats:
if agent.pendingactions.filter( if agent.pendingactions.filter(
@@ -153,37 +143,7 @@ def auto_self_agent_update_task():
"url": url, "url": url,
}, },
) )
sleep(10) sleep(5)
@app.task
def update_salt_minion_task():
q = Agent.objects.all()
agents = [
i.pk
for i in q
if pyver.parse(i.version) >= pyver.parse("0.11.0")
and pyver.parse(i.salt_ver) < pyver.parse(settings.LATEST_SALT_VER)
]
chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_async(func="win_agent.update_salt")
sleep(20)
@app.task
def get_wmi_detail_task(pk):
agent = Agent.objects.get(pk=pk)
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False))
else:
agent.salt_api_async(timeout=30, func="win_agent.local_sys_info")
return "ok"
@app.task @app.task
@@ -209,25 +169,6 @@ def batch_sync_modules_task():
sleep(10) sleep(10)
@app.task
def batch_sysinfo_task():
# update system info using WMI
agents = Agent.objects.all()
agents_nats = [agent for agent in agents if agent.has_nats]
minions = [
agent.salt_id
for agent in agents
if not agent.has_nats and pyver.parse(agent.version) >= pyver.parse("0.11.0")
]
if minions:
Agent.salt_batch_async(minions=minions, func="win_agent.local_sys_info")
for agent in agents_nats:
asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False))
@app.task @app.task
def uninstall_agent_task(salt_id, has_nats): def uninstall_agent_task(salt_id, has_nats):
attempts = 0 attempts = 0
@@ -331,9 +272,12 @@ def agent_recovery_sms_task(pk):
@app.task @app.task
def agent_outages_task(): def agent_outages_task():
agents = Agent.objects.only("pk") agents = Agent.objects.only(
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
)
for agent in agents: for agent in agents:
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue": if agent.status == "overdue":
outages = AgentOutage.objects.filter(agent=agent) outages = AgentOutage.objects.filter(agent=agent)
if outages and outages.last().is_active: if outages and outages.last().is_active:

View File

@@ -14,11 +14,8 @@ from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent from .models import Agent
from .tasks import ( from .tasks import (
auto_self_agent_update_task, auto_self_agent_update_task,
update_salt_minion_task,
get_wmi_detail_task,
sync_salt_modules_task, sync_salt_modules_task,
batch_sync_modules_task, batch_sync_modules_task,
batch_sysinfo_task,
OLD_64_PY_AGENT, OLD_64_PY_AGENT,
OLD_32_PY_AGENT, OLD_32_PY_AGENT,
) )
@@ -33,7 +30,7 @@ class TestAgentViews(TacticalTestCase):
client = baker.make("clients.Client", name="Google") client = baker.make("clients.Client", name="Google")
site = baker.make("clients.Site", client=client, name="LA Office") site = baker.make("clients.Site", client=client, name="LA Office")
self.agent = baker.make_recipe( self.agent = baker.make_recipe(
"agents.online_agent", site=site, version="1.1.0" "agents.online_agent", site=site, version="1.1.1"
) )
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent) baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
@@ -186,10 +183,10 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_power_action(self, nats_cmd): def test_reboot_now(self, nats_cmd):
url = f"/agents/poweraction/" url = f"/agents/reboot/"
data = {"pk": self.agent.pk, "action": "rebootnow"} data = {"pk": self.agent.pk}
nats_cmd.return_value = "ok" nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@@ -222,30 +219,37 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url) self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.nats_cmd")
def test_reboot_later(self, mock_ret): def test_reboot_later(self, nats_cmd):
url = f"/agents/rebootlater/" url = f"/agents/reboot/"
data = { data = {
"pk": self.agent.pk, "pk": self.agent.pk,
"datetime": "2025-08-29 18:41", "datetime": "2025-08-29 18:41",
} }
mock_ret.return_value = True nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json") r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
self.assertEqual(r.data["agent"], self.agent.hostname) self.assertEqual(r.data["agent"], self.agent.hostname)
mock_ret.return_value = "failed" nats_data = {
r = self.client.post(url, data, format="json") "func": "schedtask",
self.assertEqual(r.status_code, 400) "schedtaskpayload": {
"type": "schedreboot",
"trigger": "once",
"name": r.data["task_name"],
"year": 2025,
"month": "August",
"day": 29,
"hour": 18,
"min": 41,
},
}
nats_cmd.assert_called_with(nats_data, timeout=10)
mock_ret.return_value = "timeout" nats_cmd.return_value = "error creating task"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
mock_ret.return_value = False
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
@@ -253,12 +257,12 @@ class TestAgentViews(TacticalTestCase):
"pk": self.agent.pk, "pk": self.agent.pk,
"datetime": "rm -rf /", "datetime": "rm -rf /",
} }
r = self.client.post(url, data_invalid, format="json") r = self.client.patch(url, data_invalid, format="json")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Invalid date") self.assertEqual(r.data, "Invalid date")
self.check_not_authenticated("post", url) self.check_not_authenticated("patch", url)
@patch("os.path.exists") @patch("os.path.exists")
@patch("subprocess.run") @patch("subprocess.run")
@@ -428,7 +432,14 @@ class TestAgentViews(TacticalTestCase):
self.assertIn("&viewmode=13", r.data["file"]) self.assertIn("&viewmode=13", r.data["file"])
self.assertIn("&viewmode=12", r.data["terminal"]) self.assertIn("&viewmode=12", r.data["terminal"])
self.assertIn("&viewmode=11", r.data["control"]) self.assertIn("&viewmode=11", r.data["control"])
self.assertIn("mstsc.html?login=", r.data["webrdp"])
self.assertIn("&gotonode=", r.data["file"])
self.assertIn("&gotonode=", r.data["terminal"])
self.assertIn("&gotonode=", r.data["control"])
self.assertIn("?login=", r.data["file"])
self.assertIn("?login=", r.data["terminal"])
self.assertIn("?login=", r.data["control"])
self.assertEqual(self.agent.hostname, r.data["hostname"]) self.assertEqual(self.agent.hostname, r.data["hostname"])
self.assertEqual(self.agent.client.name, r.data["client"]) self.assertEqual(self.agent.client.name, r.data["client"])
@@ -739,19 +750,6 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate() self.authenticate()
self.setup_coresettings() self.setup_coresettings()
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.Agent.salt_api_async", return_value=None)
def test_get_wmi_detail_task(self, salt_api_async, nats_cmd):
self.agent_salt = baker.make_recipe("agents.agent", version="1.0.2")
ret = get_wmi_detail_task.s(self.agent_salt.pk).apply()
salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info")
self.assertEqual(ret.status, "SUCCESS")
self.agent_nats = baker.make_recipe("agents.agent", version="1.1.0")
ret = get_wmi_detail_task.s(self.agent_nats.pk).apply()
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.salt_api_cmd")
def test_sync_salt_modules_task(self, salt_api_cmd): def test_sync_salt_modules_task(self, salt_api_cmd):
self.agent = baker.make_recipe("agents.agent") self.agent = baker.make_recipe("agents.agent")
@@ -787,83 +785,6 @@ class TestAgentTasks(TacticalTestCase):
self.assertEqual(salt_batch_async.call_count, 4) self.assertEqual(salt_batch_async.call_count, 4)
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async, nats_cmd):
self.agents_nats = baker.make_recipe(
"agents.agent", version="1.1.0", _quantity=20
)
# test nats
ret = batch_sysinfo_task.s().apply()
self.assertEqual(nats_cmd.call_count, 20)
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
self.assertEqual(ret.status, "SUCCESS")
self.agents_salt = baker.make_recipe(
"agents.agent", version="1.0.2", _quantity=70
)
minions = [i.salt_id for i in self.agents_salt]
ret = batch_sysinfo_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 1)
salt_batch_async.assert_called_with(
minions=minions, func="win_agent.local_sys_info"
)
self.assertEqual(ret.status, "SUCCESS")
salt_batch_async.reset_mock()
[i.delete() for i in self.agents_salt]
# test old agents, should not run
self.agents_old = baker.make_recipe(
"agents.agent", version="0.10.2", _quantity=70
)
ret = batch_sysinfo_task.s().apply()
salt_batch_async.assert_not_called()
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_update_salt_minion_task(self, mock_sleep, salt_api_async):
# test agents that need salt update
self.agents = baker.make_recipe(
"agents.agent",
version=settings.LATEST_AGENT_VER,
salt_ver="1.0.3",
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(salt_api_async.call_count, 53)
self.assertEqual(ret.status, "SUCCESS")
[i.delete() for i in self.agents]
salt_api_async.reset_mock()
# test agents that need salt update but agent version too low
self.agents = baker.make_recipe(
"agents.agent",
version="0.10.2",
salt_ver="1.0.3",
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(ret.status, "SUCCESS")
salt_api_async.assert_not_called()
[i.delete() for i in self.agents]
salt_api_async.reset_mock()
# test agents already on latest salt ver
self.agents = baker.make_recipe(
"agents.agent",
version=settings.LATEST_AGENT_VER,
salt_ver=settings.LATEST_SALT_VER,
_quantity=53,
)
ret = update_salt_minion_task.s().apply()
self.assertEqual(ret.status, "SUCCESS")
salt_api_async.assert_not_called()
@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

@@ -12,7 +12,6 @@ urlpatterns = [
path("<pk>/agentdetail/", views.agent_detail), path("<pk>/agentdetail/", views.agent_detail),
path("<int:pk>/meshcentral/", views.meshcentral), path("<int:pk>/meshcentral/", views.meshcentral),
path("<str:arch>/getmeshexe/", views.get_mesh_exe), path("<str:arch>/getmeshexe/", views.get_mesh_exe),
path("poweraction/", views.power_action),
path("uninstall/", views.uninstall), path("uninstall/", views.uninstall),
path("editagent/", views.edit_agent), path("editagent/", views.edit_agent),
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log), path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
@@ -20,7 +19,7 @@ urlpatterns = [
path("updateagents/", views.update_agents), path("updateagents/", views.update_agents),
path("<pk>/getprocs/", views.get_processes), path("<pk>/getprocs/", views.get_processes),
path("<pk>/<pid>/killproc/", views.kill_proc), path("<pk>/<pid>/killproc/", views.kill_proc),
path("rebootlater/", views.reboot_later), path("reboot/", views.Reboot.as_view()),
path("installagent/", views.install_agent), path("installagent/", views.install_agent),
path("<int:pk>/ping/", views.ping), path("<int:pk>/ping/", views.ping),
path("recover/", views.recover), path("recover/", views.recover),

View File

@@ -3,6 +3,8 @@ from loguru import logger
import os import os
import subprocess import subprocess
import pytz import pytz
import random
import string
import datetime as dt import datetime as dt
from packaging import version as pyver from packaging import version as pyver
@@ -18,7 +20,7 @@ from rest_framework import status, generics
from .models import Agent, AgentOutage, RecoveryAction, Note from .models import Agent, AgentOutage, RecoveryAction, Note
from core.models import CoreSettings from core.models import CoreSettings
from scripts.models import Script from scripts.models import Script
from logs.models import AuditLog from logs.models import AuditLog, PendingAction
from .serializers import ( from .serializers import (
AgentSerializer, AgentSerializer,
@@ -93,6 +95,8 @@ def uninstall(request):
@api_view(["PATCH"]) @api_view(["PATCH"])
def edit_agent(request): def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"]) agent = get_object_or_404(Agent, pk=request.data["id"])
old_site = agent.site.pk
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True) a_serializer.is_valid(raise_exception=True)
a_serializer.save() a_serializer.save()
@@ -104,6 +108,11 @@ def edit_agent(request):
p_serializer.is_valid(raise_exception=True) p_serializer.is_valid(raise_exception=True)
p_serializer.save() p_serializer.save()
# check if site changed and initiate generating correct policies
if old_site != request.data["site"]:
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
return Response("ok") return Response("ok")
@@ -119,16 +128,9 @@ def meshcentral(request, pk):
if token == "err": if token == "err":
return notify_error("Invalid mesh token") return notify_error("Invalid mesh token")
control = ( control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=11&hide=31" terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
) file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31"
terminal = (
f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=12&hide=31"
)
file = (
f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=13&hide=31"
)
webrdp = f"{core.mesh_site}/mstsc.html?login={token}&node={agent.mesh_node_id}"
AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname) AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname)
@@ -137,7 +139,6 @@ def meshcentral(request, pk):
"control": control, "control": control,
"terminal": terminal, "terminal": terminal,
"file": file, "file": file,
"webrdp": webrdp,
"status": agent.status, "status": agent.status,
"client": agent.client.name, "client": agent.client.name,
"site": agent.site.name, "site": agent.site.name,
@@ -201,19 +202,6 @@ def get_event_log(request, pk, logtype, days):
return Response(r) return Response(r)
@api_view(["POST"])
def power_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if request.data["action"] == "rebootnow":
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response("ok")
@api_view(["POST"]) @api_view(["POST"])
def send_raw_cmd(request): def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"]) agent = get_object_or_404(Agent, pk=request.data["pk"])
@@ -372,24 +360,60 @@ def overdue_action(request):
return Response(agent.hostname) return Response(agent.hostname)
@api_view(["POST"]) class Reboot(APIView):
def reboot_later(request): # reboot now
def post(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"]) agent = get_object_or_404(Agent, pk=request.data["pk"])
date_time = request.data["datetime"] if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response("ok")
# reboot later
def patch(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
try: try:
obj = dt.datetime.strptime(date_time, "%Y-%m-%d %H:%M") obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
except Exception: except Exception:
return notify_error("Invalid date") return notify_error("Invalid date")
r = agent.schedule_reboot(obj) task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
if r == "timeout": nats_data = {
return notify_error("Unable to contact the agent") "func": "schedtask",
elif r == "failed": "schedtaskpayload": {
return notify_error("Something went wrong") "type": "schedreboot",
"trigger": "once",
"name": task_name,
"year": int(dt.datetime.strftime(obj, "%Y")),
"month": dt.datetime.strftime(obj, "%B"),
"day": int(dt.datetime.strftime(obj, "%d")),
"hour": int(dt.datetime.strftime(obj, "%H")),
"min": int(dt.datetime.strftime(obj, "%M")),
},
}
return Response(r["msg"]) r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
details = {"taskname": task_name, "time": str(obj)}
PendingAction.objects.create(
agent=agent, action_type="schedreboot", details=details
)
nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
return Response(
{"time": nice_time, "agent": agent.hostname, "task_name": task_name}
)
@api_view(["POST"]) @api_view(["POST"])

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = "api"

View File

@@ -1,11 +0,0 @@
from django.urls import path
from . import views
from apiv3 import views as v3_views
urlpatterns = [
path("triggerpatchscan/", views.trigger_patch_scan),
path("<int:pk>/checkrunner/", views.CheckRunner.as_view()),
path("<int:pk>/taskrunner/", views.TaskRunner.as_view()),
path("<int:pk>/saltinfo/", views.SaltInfo.as_view()),
path("<int:pk>/meshinfo/", v3_views.MeshInfo.as_view()),
]

View File

@@ -1,149 +0,0 @@
from loguru import logger
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import (
api_view,
authentication_classes,
permission_classes,
)
from agents.models import Agent
from checks.models import Check
from autotasks.models import AutomatedTask
from winupdate.tasks import check_for_updates_task
from autotasks.serializers import TaskRunnerGetSerializer, TaskRunnerPatchSerializer
from checks.serializers import CheckRunnerGetSerializer, CheckResultsSerializer
logger.configure(**settings.LOG_CONFIG)
@api_view(["PATCH"])
@authentication_classes((TokenAuthentication,))
@permission_classes((IsAuthenticated,))
def trigger_patch_scan(request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
r = agent.salt_api_cmd(
timeout=15,
func="system.reboot",
arg=7,
kwargs={"in_seconds": True},
)
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
else:
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
else:
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
return Response("ok")
class CheckRunner(APIView):
"""
For windows agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False)
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializer(checks, many=True).data,
}
return Response(ret)
def patch(self, request, pk):
check = get_object_or_404(Check, pk=pk)
if check.check_type != "cpuload" and check.check_type != "memory":
serializer = CheckResultsSerializer(
instance=check, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
else:
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
check.handle_check(request.data)
return Response("ok")
class TaskRunner(APIView):
"""
For the windows python agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, pk):
task = get_object_or_404(AutomatedTask, pk=pk)
return Response(TaskRunnerGetSerializer(task).data)
def patch(self, request, pk):
task = get_object_or_404(AutomatedTask, pk=pk)
serializer = TaskRunnerPatchSerializer(
instance=task, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
return Response("ok")
class SaltInfo(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
ret = {
"latestVer": settings.LATEST_SALT_VER,
"currentVer": agent.salt_ver,
"salt_id": agent.salt_id,
}
return Response(ret)
def patch(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
agent.salt_ver = request.data["ver"]
agent.save(update_fields=["salt_ver"])
return Response("ok")

View File

@@ -45,15 +45,11 @@ class TestAPIv3(TacticalTestCase):
def test_get_mesh_info(self): def test_get_mesh_info(self):
url = f"/api/v3/{self.agent.pk}/meshinfo/" url = f"/api/v3/{self.agent.pk}/meshinfo/"
url2 = f"/api/v1/{self.agent.pk}/meshinfo/"
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.get(url2)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
self.check_not_authenticated("get", url2)
def test_get_winupdater(self): def test_get_winupdater(self):
url = f"/api/v3/{self.agent.agent_id}/winupdater/" url = f"/api/v3/{self.agent.agent_id}/winupdater/"

View File

@@ -14,4 +14,6 @@ urlpatterns = [
path("newagent/", views.NewAgent.as_view()), path("newagent/", views.NewAgent.as_view()),
path("winupdater/", views.WinUpdater.as_view()), path("winupdater/", views.WinUpdater.as_view()),
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()), path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
path("software/", views.Software.as_view()),
path("installer/", views.Installer.as_view()),
] ]

View File

@@ -2,12 +2,12 @@ import asyncio
import os import os
import requests import requests
from loguru import logger from loguru import logger
from packaging import version as pyver
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from django.http import HttpResponse from django.http import HttpResponse
from rest_framework import serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@@ -20,6 +20,7 @@ from checks.models import Check
from autotasks.models import AutomatedTask 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 checks.serializers import CheckRunnerGetSerializerV3 from checks.serializers import CheckRunnerGetSerializerV3
from agents.serializers import WinAgentSerializer from agents.serializers import WinAgentSerializer
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
@@ -28,13 +29,12 @@ from winupdate.serializers import ApprovedUpdateSerializer
from agents.tasks import ( from agents.tasks import (
agent_recovery_email_task, agent_recovery_email_task,
agent_recovery_sms_task, agent_recovery_sms_task,
get_wmi_detail_task,
sync_salt_modules_task, sync_salt_modules_task,
) )
from winupdate.tasks import check_for_updates_task from winupdate.tasks import check_for_updates_task
from software.tasks import get_installed_software, install_chocolatey from software.tasks import install_chocolatey
from checks.utils import bytes2human from checks.utils import bytes2human
from tacticalrmm.utils import notify_error, reload_nats from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
@@ -123,8 +123,6 @@ class Hello(APIView):
serializer.save(last_seen=djangotime.now()) serializer.save(last_seen=djangotime.now())
sync_salt_modules_task.delay(agent.pk) sync_salt_modules_task.delay(agent.pk)
get_installed_software.delay(agent.pk)
get_wmi_detail_task.delay(agent.pk)
check_for_updates_task.apply_async( check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": True} queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
) )
@@ -386,7 +384,15 @@ class MeshInfo(APIView):
def patch(self, request, pk): def patch(self, request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
agent.mesh_node_id = request.data["nodeidhex"]
if "nodeidhex" in request.data:
# agent <= 1.1.0
nodeid = request.data["nodeidhex"]
else:
# agent >= 1.1.1
nodeid = request.data["nodeid"]
agent.mesh_node_id = nodeid
agent.save(update_fields=["mesh_node_id"]) agent.save(update_fields=["mesh_node_id"])
return Response("ok") return Response("ok")
@@ -476,3 +482,42 @@ class NewAgent(APIView):
"token": token.key, "token": token.key,
} }
) )
class Software(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return Response("ok")
class Installer(APIView):
def get(self, request):
# used to check if token is valid. will return 401 if not
return Response("ok")
def post(self, request):
if "version" not in request.data:
return notify_error("Invalid data")
ver = request.data["version"]
if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER):
return notify_error(
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
)
return Response("ok")

View File

@@ -1051,10 +1051,13 @@ class TestPolicyTasks(TacticalTestCase):
for task in tasks: for task in tasks:
run_win_task.assert_any_call(task.id) run_win_task.assert_any_call(task.id)
def test_update_policy_tasks(self): @patch("agents.models.Agent.nats_cmd")
def test_update_policy_tasks(self, nats_cmd):
from .tasks import update_policy_task_fields_task from .tasks import update_policy_task_fields_task
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
nats_cmd.return_value = "ok"
# setup data # setup data
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
tasks = baker.make( tasks = baker.make(

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-11-29 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0008_auto_20201030_1515'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='run_time_bit_weekdays',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,33 @@
from django.db import migrations
from tacticalrmm.utils import get_bit_days
DAYS_OF_WEEK = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday",
}
def migrate_days(apps, schema_editor):
AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
for task in AutomatedTask.objects.exclude(run_time_days__isnull=True).exclude(
run_time_days=[]
):
run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days]
task.run_time_bit_weekdays = get_bit_days(run_days)
task.save(update_fields=["run_time_bit_weekdays"])
class Migration(migrations.Migration):
dependencies = [
("autotasks", "0009_automatedtask_run_time_bit_weekdays"),
]
operations = [
migrations.RunPython(migrate_days),
]

View File

@@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField
from django.db.models.fields import DateTimeField from django.db.models.fields import DateTimeField
from automation.models import Policy from automation.models import Policy
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
from tacticalrmm.utils import bitdays_to_string
RUN_TIME_DAY_CHOICES = [ RUN_TIME_DAY_CHOICES = [
(0, "Monday"), (0, "Monday"),
@@ -69,6 +70,8 @@ class AutomatedTask(BaseAuditModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
run_time_bit_weekdays = models.IntegerField(null=True, blank=True)
# run_time_days is deprecated, use bit weekdays
run_time_days = ArrayField( run_time_days = ArrayField(
models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True), models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True),
null=True, null=True,
@@ -107,20 +110,11 @@ class AutomatedTask(BaseAuditModel):
elif self.task_type == "runonce": elif self.task_type == "runonce":
return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}' return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}'
elif self.task_type == "scheduled": elif self.task_type == "scheduled":
ret = []
for i in self.run_time_days:
for j in RUN_TIME_DAY_CHOICES:
if i in j:
ret.append(j[1][0:3])
run_time_nice = dt.datetime.strptime( run_time_nice = dt.datetime.strptime(
self.run_time_minute, "%H:%M" self.run_time_minute, "%H:%M"
).strftime("%I:%M %p") ).strftime("%I:%M %p")
if len(ret) == 7: days = bitdays_to_string(self.run_time_bit_weekdays)
return f"Every day at {run_time_nice}"
else:
days = ",".join(ret)
return f"{days} at {run_time_nice}" return f"{days} at {run_time_nice}"
@property @property
@@ -169,6 +163,7 @@ class AutomatedTask(BaseAuditModel):
name=self.name, name=self.name,
run_time_days=self.run_time_days, run_time_days=self.run_time_days,
run_time_minute=self.run_time_minute, run_time_minute=self.run_time_minute,
run_time_bit_weekdays=self.run_time_bit_weekdays,
run_time_date=self.run_time_date, run_time_date=self.run_time_date,
task_type=self.task_type, task_type=self.task_type,
win_task_name=self.win_task_name, win_task_name=self.win_task_name,

View File

@@ -1,3 +1,5 @@
import asyncio
import datetime as dt
from loguru import logger from loguru import logger
from tacticalrmm.celery import app from tacticalrmm.celery import app
from django.conf import settings from django.conf import settings
@@ -9,44 +11,26 @@ from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
DAYS_OF_WEEK = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday",
}
@app.task @app.task
def create_win_task_schedule(pk, pending_action=False): def create_win_task_schedule(pk, pending_action=False):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
if task.task_type == "scheduled": if task.task_type == "scheduled":
run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days] nats_data = {
"func": "schedtask",
r = task.agent.salt_api_cmd( "schedtaskpayload": {
timeout=20, "type": "rmm",
func="task.create_task", "trigger": "weekly",
arg=[ "weekdays": task.run_time_bit_weekdays,
f"name={task.win_task_name}", "pk": task.pk,
"force=True", "name": task.win_task_name,
"action_type=Execute", "hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
f'arguments="-m taskrunner -p {task.pk}"', },
"start_in=C:\\Program Files\\TacticalAgent", }
"trigger_type=Weekly",
f'start_time="{task.run_time_minute}"',
"ac_only=False",
"stop_if_on_batteries=False",
],
kwargs={"days_of_week": run_days},
)
elif task.task_type == "runonce": elif task.task_type == "runonce":
# check if scheduled time is in the past # check if scheduled time is in the past
agent_tz = pytz.timezone(task.agent.timezone) agent_tz = pytz.timezone(task.agent.timezone)
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc) task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
@@ -57,45 +41,36 @@ def create_win_task_schedule(pk, pending_action=False):
) + djangotime.timedelta(minutes=5) ) + djangotime.timedelta(minutes=5)
task.save() task.save()
r = task.agent.salt_api_cmd( nats_data = {
timeout=20, "func": "schedtask",
func="task.create_task", "schedtaskpayload": {
arg=[ "type": "rmm",
f"name={task.win_task_name}", "trigger": "once",
"force=True", "pk": task.pk,
"action_type=Execute", "name": task.win_task_name,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
f'arguments="-m taskrunner -p {task.pk}"', "month": dt.datetime.strftime(task.run_time_date, "%B"),
"start_in=C:\\Program Files\\TacticalAgent", "day": int(dt.datetime.strftime(task.run_time_date, "%d")),
"trigger_type=Once", "hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
f'start_date="{task.run_time_date.strftime("%Y-%m-%d")}"', "min": int(dt.datetime.strftime(task.run_time_date, "%M")),
f'start_time="{task.run_time_date.strftime("%H:%M")}"', },
"ac_only=False", }
"stop_if_on_batteries=False",
"start_when_available=True",
],
)
elif task.task_type == "checkfailure" or task.task_type == "manual": elif task.task_type == "checkfailure" or task.task_type == "manual":
r = task.agent.salt_api_cmd( nats_data = {
timeout=20, "func": "schedtask",
func="task.create_task", "schedtaskpayload": {
arg=[ "type": "rmm",
f"name={task.win_task_name}", "trigger": "manual",
"force=True", "pk": task.pk,
"action_type=Execute", "name": task.win_task_name,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', },
f'arguments="-m taskrunner -p {task.pk}"', }
"start_in=C:\\Program Files\\TacticalAgent", else:
"trigger_type=Once", return "error"
'start_date="1975-01-01"',
'start_time="01:00"',
"ac_only=False",
"stop_if_on_batteries=False",
],
)
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
# don't create pending action if this task was initiated by a pending action # don't create pending action if this task was initiated by a pending action
if not pending_action: if not pending_action:
PendingAction( PendingAction(
@@ -129,13 +104,16 @@ def create_win_task_schedule(pk, pending_action=False):
def enable_or_disable_win_task(pk, action, pending_action=False): def enable_or_disable_win_task(pk, action, pending_action=False):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
r = task.agent.salt_api_cmd( nats_data = {
timeout=20, "func": "enableschedtask",
func="task.edit_task", "schedtaskpayload": {
arg=[f"name={task.win_task_name}", f"enabled={action}"], "name": task.win_task_name,
) "enabled": action,
},
}
r = asyncio.run(task.agent.nats_cmd(nats_data))
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): if r != "ok":
# don't create pending action if this task was initiated by a pending action # don't create pending action if this task was initiated by a pending action
if not pending_action: if not pending_action:
PendingAction( PendingAction(
@@ -150,9 +128,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
task.sync_status = "notsynced" task.sync_status = "notsynced"
task.save(update_fields=["sync_status"]) task.save(update_fields=["sync_status"])
logger.error(
f"Unable to update the scheduled task {task.win_task_name} on {task.agent.hostname}. It will be updated when the agent checks in."
)
return return
# clear pending action since it was successful # clear pending action since it was successful
@@ -163,7 +138,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
task.sync_status = "synced" task.sync_status = "synced"
task.save(update_fields=["sync_status"]) task.save(update_fields=["sync_status"])
logger.info(f"{task.agent.hostname} task {task.name} was edited.")
return "ok" return "ok"
@@ -171,13 +145,13 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
def delete_win_task_schedule(pk, pending_action=False): def delete_win_task_schedule(pk, pending_action=False):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
r = task.agent.salt_api_cmd( nats_data = {
timeout=20, "func": "delschedtask",
func="task.delete_task", "schedtaskpayload": {"name": task.win_task_name},
arg=[f"name={task.win_task_name}"], }
) r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): if r != "ok":
# don't create pending action if this task was initiated by a pending action # don't create pending action if this task was initiated by a pending action
if not pending_action: if not pending_action:
PendingAction( PendingAction(
@@ -188,9 +162,6 @@ def delete_win_task_schedule(pk, pending_action=False):
task.sync_status = "pendingdeletion" task.sync_status = "pendingdeletion"
task.save(update_fields=["sync_status"]) task.save(update_fields=["sync_status"])
logger.error(
f"Unable to delete scheduled task {task.win_task_name} on {task.agent.hostname}. It was marked pending deletion and will be removed when the agent checks in."
)
return return
# complete pending action since it was successful # complete pending action since it was successful
@@ -200,15 +171,13 @@ def delete_win_task_schedule(pk, pending_action=False):
pendingaction.save(update_fields=["status"]) pendingaction.save(update_fields=["status"])
task.delete() task.delete()
logger.info(f"{task.agent.hostname} task {task.name} was deleted.")
return "ok" return "ok"
@app.task @app.task
def run_win_task(pk): def run_win_task(pk):
# TODO deprecated, remove this function once salt gone
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
r = task.agent.salt_api_async(func="task.run", arg=[f"name={task.win_task_name}"]) asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
return "ok" return "ok"
@@ -220,18 +189,9 @@ def remove_orphaned_win_tasks(agentpk):
logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.") logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.")
r = agent.salt_api_cmd( r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
timeout=15,
func="task.list_tasks",
)
if r == "timeout" or r == "error": if not isinstance(r, list) and not r: # empty list
logger.error(
f"Unable to clean up scheduled tasks on {agent.hostname}. Agent might be offline"
)
return "errtimeout"
if not isinstance(r, list):
logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}") logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}")
return "notlist" return "notlist"
@@ -240,7 +200,7 @@ def remove_orphaned_win_tasks(agentpk):
exclude_tasks = ( exclude_tasks = (
"TacticalRMM_fixmesh", "TacticalRMM_fixmesh",
"TacticalRMM_SchedReboot", "TacticalRMM_SchedReboot",
"TacticalRMM_saltwatchdog", # will be implemented in future "TacticalRMM_sync",
) )
for task in r: for task in r:
@@ -250,16 +210,16 @@ def remove_orphaned_win_tasks(agentpk):
if task.startswith("TacticalRMM_") and task not in agent_task_names: if task.startswith("TacticalRMM_") and task not in agent_task_names:
# delete task since it doesn't exist in UI # delete task since it doesn't exist in UI
ret = agent.salt_api_cmd( nats_data = {
timeout=20, "func": "delschedtask",
func="task.delete_task", "schedtaskpayload": {"name": task},
arg=[f"name={task}"], }
) ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if isinstance(ret, bool) and ret is True: if ret != "ok":
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
else:
logger.error( logger.error(
f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}" f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}"
) )
else:
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
logger.info(f"Orphaned task cleanup finished on {agent.hostname}") logger.info(f"Orphaned task cleanup finished on {agent.hostname}")

View File

@@ -1,3 +1,4 @@
import datetime as dt
from unittest.mock import patch, call from unittest.mock import patch, call
from model_bakery import baker from model_bakery import baker
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
@@ -25,9 +26,9 @@ class TestAutotaskViews(TacticalTestCase):
# setup data # setup data
script = baker.make_recipe("scripts.script") script = baker.make_recipe("scripts.script")
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")
agent_old = baker.make_recipe("agents.agent", version="0.9.0")
policy = baker.make("automation.Policy") policy = baker.make("automation.Policy")
check = baker.make_recipe("checks.diskspace_check", agent=agent) check = baker.make_recipe("checks.diskspace_check", agent=agent)
old_agent = baker.make_recipe("agents.agent", version="1.1.0")
# test script set to invalid pk # test script set to invalid pk
data = {"autotask": {"script": 500}} data = {"autotask": {"script": 500}}
@@ -50,10 +51,10 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.post(url, data, format="json") resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
# test invalid agent version # test old agent version
data = { data = {
"autotask": {"script": script.id, "script_args": ["args"]}, "autotask": {"script": script.id},
"agent": agent_old.id, "agent": old_agent.id,
} }
resp = self.client.post(url, data, format="json") resp = self.client.post(url, data, format="json")
@@ -63,7 +64,7 @@ class TestAutotaskViews(TacticalTestCase):
data = { data = {
"autotask": { "autotask": {
"name": "Test Task Scheduled with Assigned Check", "name": "Test Task Scheduled with Assigned Check",
"run_time_days": [0, 1, 2], "run_time_days": ["Sunday", "Monday", "Friday"],
"run_time_minute": "10:00", "run_time_minute": "10:00",
"timeout": 120, "timeout": 120,
"enabled": True, "enabled": True,
@@ -84,6 +85,7 @@ class TestAutotaskViews(TacticalTestCase):
data = { data = {
"autotask": { "autotask": {
"name": "Test Task Manual", "name": "Test Task Manual",
"run_time_days": [],
"timeout": 120, "timeout": 120,
"enabled": True, "enabled": True,
"script": script.id, "script": script.id,
@@ -213,8 +215,8 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
self.authenticate() self.authenticate()
self.setup_coresettings() self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.nats_cmd")
def test_remove_orphaned_win_task(self, salt_api_cmd): def test_remove_orphaned_win_task(self, nats_cmd):
self.agent = baker.make_recipe("agents.agent") self.agent = baker.make_recipe("agents.agent")
self.task1 = AutomatedTask.objects.create( self.task1 = AutomatedTask.objects.create(
agent=self.agent, agent=self.agent,
@@ -222,20 +224,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
win_task_name=AutomatedTask.generate_task_name(), win_task_name=AutomatedTask.generate_task_name(),
) )
salt_api_cmd.return_value = "timeout"
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
self.assertEqual(ret.result, "errtimeout")
salt_api_cmd.return_value = "error"
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
self.assertEqual(ret.result, "errtimeout")
salt_api_cmd.return_value = "task not found in"
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
self.assertEqual(ret.result, "notlist")
salt_api_cmd.reset_mock()
# test removing an orphaned task # test removing an orphaned task
win_tasks = [ win_tasks = [
"Adobe Acrobat Update Task", "Adobe Acrobat Update Task",
@@ -250,50 +238,54 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
] ]
self.calls = [ self.calls = [
call(timeout=15, func="task.list_tasks"), call({"func": "listschedtasks"}, timeout=10),
call( call(
timeout=20, {
func="task.delete_task", "func": "delschedtask",
arg=["name=TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"], "schedtaskpayload": {
"name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"
},
},
timeout=10,
), ),
] ]
salt_api_cmd.side_effect = [win_tasks, True] nats_cmd.side_effect = [win_tasks, "ok"]
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
self.assertEqual(salt_api_cmd.call_count, 2) self.assertEqual(nats_cmd.call_count, 2)
salt_api_cmd.assert_has_calls(self.calls) nats_cmd.assert_has_calls(self.calls)
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
# test salt delete_task fail # test nats delete task fail
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
salt_api_cmd.side_effect = [win_tasks, False] nats_cmd.side_effect = [win_tasks, "error deleting task"]
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
salt_api_cmd.assert_has_calls(self.calls) nats_cmd.assert_has_calls(self.calls)
self.assertEqual(salt_api_cmd.call_count, 2) self.assertEqual(nats_cmd.call_count, 2)
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
# no orphaned tasks # no orphaned tasks
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb") win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb")
salt_api_cmd.side_effect = [win_tasks, True] nats_cmd.side_effect = [win_tasks, "ok"]
ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() ret = remove_orphaned_win_tasks.s(self.agent.pk).apply()
self.assertEqual(salt_api_cmd.call_count, 1) self.assertEqual(nats_cmd.call_count, 1)
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_async") @patch("agents.models.Agent.nats_cmd")
def test_run_win_task(self, salt_api_async): def test_run_win_task(self, nats_cmd):
self.agent = baker.make_recipe("agents.agent") self.agent = baker.make_recipe("agents.agent")
self.task1 = AutomatedTask.objects.create( self.task1 = AutomatedTask.objects.create(
agent=self.agent, agent=self.agent,
name="test task 1", name="test task 1",
win_task_name=AutomatedTask.generate_task_name(), win_task_name=AutomatedTask.generate_task_name(),
) )
salt_api_async.return_value = "Response 200" nats_cmd.return_value = "ok"
ret = run_win_task.s(self.task1.pk).apply() ret = run_win_task.s(self.task1.pk).apply()
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.nats_cmd")
def test_create_win_task_schedule(self, salt_api_cmd): def test_create_win_task_schedule(self, nats_cmd):
self.agent = baker.make_recipe("agents.agent") self.agent = baker.make_recipe("agents.agent")
task_name = AutomatedTask.generate_task_name() task_name = AutomatedTask.generate_task_name()
@@ -303,46 +295,32 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
name="test task 1", name="test task 1",
win_task_name=task_name, win_task_name=task_name,
task_type="scheduled", task_type="scheduled",
run_time_days=[0, 1, 6], run_time_bit_weekdays=127,
run_time_minute="21:55", run_time_minute="21:55",
) )
self.assertEqual(self.task1.sync_status, "notsynced") self.assertEqual(self.task1.sync_status, "notsynced")
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
self.assertEqual(salt_api_cmd.call_count, 1) self.assertEqual(nats_cmd.call_count, 1)
salt_api_cmd.assert_called_with( nats_cmd.assert_called_with(
timeout=20, {
func="task.create_task", "func": "schedtask",
arg=[ "schedtaskpayload": {
f"name={task_name}", "type": "rmm",
"force=True", "trigger": "weekly",
"action_type=Execute", "weekdays": 127,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "pk": self.task1.pk,
f'arguments="-m taskrunner -p {self.task1.pk}"', "name": task_name,
"start_in=C:\\Program Files\\TacticalAgent", "hour": 21,
"trigger_type=Weekly", "min": 55,
'start_time="21:55"', },
"ac_only=False", },
"stop_if_on_batteries=False", timeout=10,
],
kwargs={"days_of_week": ["Monday", "Tuesday", "Sunday"]},
) )
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "synced") self.assertEqual(self.task1.sync_status, "synced")
salt_api_cmd.return_value = "timeout" nats_cmd.return_value = "timeout"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
self.assertEqual(ret.status, "SUCCESS")
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "notsynced")
salt_api_cmd.return_value = "error"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
self.assertEqual(ret.status, "SUCCESS")
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "notsynced")
salt_api_cmd.return_value = False
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
@@ -353,7 +331,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
agent=self.agent, action_type="taskaction" agent=self.agent, action_type="taskaction"
) )
self.assertEqual(self.pending_action.status, "pending") self.assertEqual(self.pending_action.status, "pending")
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s( ret = create_win_task_schedule.s(
pk=self.task1.pk, pending_action=self.pending_action.pk pk=self.task1.pk, pending_action=self.pending_action.pk
).apply() ).apply()
@@ -362,7 +340,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
self.assertEqual(self.pending_action.status, "completed") self.assertEqual(self.pending_action.status, "completed")
# test runonce with future date # test runonce with future date
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
task_name = AutomatedTask.generate_task_name() task_name = AutomatedTask.generate_task_name()
run_time_date = djangotime.now() + djangotime.timedelta(hours=22) run_time_date = djangotime.now() + djangotime.timedelta(hours=22)
self.task2 = AutomatedTask.objects.create( self.task2 = AutomatedTask.objects.create(
@@ -372,30 +350,29 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
task_type="runonce", task_type="runonce",
run_time_date=run_time_date, run_time_date=run_time_date,
) )
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
salt_api_cmd.assert_called_with( nats_cmd.assert_called_with(
timeout=20, {
func="task.create_task", "func": "schedtask",
arg=[ "schedtaskpayload": {
f"name={task_name}", "type": "rmm",
"force=True", "trigger": "once",
"action_type=Execute", "pk": self.task2.pk,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "name": task_name,
f'arguments="-m taskrunner -p {self.task2.pk}"', "year": int(dt.datetime.strftime(self.task2.run_time_date, "%Y")),
"start_in=C:\\Program Files\\TacticalAgent", "month": dt.datetime.strftime(self.task2.run_time_date, "%B"),
"trigger_type=Once", "day": int(dt.datetime.strftime(self.task2.run_time_date, "%d")),
f'start_date="{run_time_date.strftime("%Y-%m-%d")}"', "hour": int(dt.datetime.strftime(self.task2.run_time_date, "%H")),
f'start_time="{run_time_date.strftime("%H:%M")}"', "min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
"ac_only=False", },
"stop_if_on_batteries=False", },
"start_when_available=True", timeout=10,
],
) )
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
# test runonce with date in the past # test runonce with date in the past
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
task_name = AutomatedTask.generate_task_name() task_name = AutomatedTask.generate_task_name()
run_time_date = djangotime.now() - djangotime.timedelta(days=13) run_time_date = djangotime.now() - djangotime.timedelta(days=13)
self.task3 = AutomatedTask.objects.create( self.task3 = AutomatedTask.objects.create(
@@ -405,31 +382,13 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
task_type="runonce", task_type="runonce",
run_time_date=run_time_date, run_time_date=run_time_date,
) )
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
self.task3 = AutomatedTask.objects.get(pk=self.task3.pk) self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
salt_api_cmd.assert_called_with(
timeout=20,
func="task.create_task",
arg=[
f"name={task_name}",
"force=True",
"action_type=Execute",
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"',
f'arguments="-m taskrunner -p {self.task3.pk}"',
"start_in=C:\\Program Files\\TacticalAgent",
"trigger_type=Once",
f'start_date="{self.task3.run_time_date.strftime("%Y-%m-%d")}"',
f'start_time="{self.task3.run_time_date.strftime("%H:%M")}"',
"ac_only=False",
"stop_if_on_batteries=False",
"start_when_available=True",
],
)
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
# test checkfailure # test checkfailure
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent) self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent)
task_name = AutomatedTask.generate_task_name() task_name = AutomatedTask.generate_task_name()
self.task4 = AutomatedTask.objects.create( self.task4 = AutomatedTask.objects.create(
@@ -439,29 +398,24 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
task_type="checkfailure", task_type="checkfailure",
assigned_check=self.check, assigned_check=self.check,
) )
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
salt_api_cmd.assert_called_with( nats_cmd.assert_called_with(
timeout=20, {
func="task.create_task", "func": "schedtask",
arg=[ "schedtaskpayload": {
f"name={task_name}", "type": "rmm",
"force=True", "trigger": "manual",
"action_type=Execute", "pk": self.task4.pk,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "name": task_name,
f'arguments="-m taskrunner -p {self.task4.pk}"', },
"start_in=C:\\Program Files\\TacticalAgent", },
"trigger_type=Once", timeout=10,
'start_date="1975-01-01"',
'start_time="01:00"',
"ac_only=False",
"stop_if_on_batteries=False",
],
) )
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")
# test manual # test manual
salt_api_cmd.reset_mock() nats_cmd.reset_mock()
task_name = AutomatedTask.generate_task_name() task_name = AutomatedTask.generate_task_name()
self.task5 = AutomatedTask.objects.create( self.task5 = AutomatedTask.objects.create(
agent=self.agent, agent=self.agent,
@@ -469,23 +423,18 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
win_task_name=task_name, win_task_name=task_name,
task_type="manual", task_type="manual",
) )
salt_api_cmd.return_value = True nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply() ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
salt_api_cmd.assert_called_with( nats_cmd.assert_called_with(
timeout=20, {
func="task.create_task", "func": "schedtask",
arg=[ "schedtaskpayload": {
f"name={task_name}", "type": "rmm",
"force=True", "trigger": "manual",
"action_type=Execute", "pk": self.task5.pk,
'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', "name": task_name,
f'arguments="-m taskrunner -p {self.task5.pk}"', },
"start_in=C:\\Program Files\\TacticalAgent", },
"trigger_type=Once", timeout=10,
'start_date="1975-01-01"',
'start_time="01:00"',
"ac_only=False",
"stop_if_on_batteries=False",
],
) )
self.assertEqual(ret.status, "SUCCESS") self.assertEqual(ret.status, "SUCCESS")

View File

@@ -20,7 +20,7 @@ from .tasks import (
delete_win_task_schedule, delete_win_task_schedule,
enable_or_disable_win_task, enable_or_disable_win_task,
) )
from tacticalrmm.utils import notify_error from tacticalrmm.utils import notify_error, get_bit_days
class AddAutoTask(APIView): class AddAutoTask(APIView):
@@ -38,17 +38,20 @@ class AddAutoTask(APIView):
parent = {"policy": policy} parent = {"policy": policy}
else: else:
agent = get_object_or_404(Agent, pk=data["agent"]) agent = get_object_or_404(Agent, pk=data["agent"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
parent = {"agent": agent} parent = {"agent": agent}
added = "0.11.0"
if data["autotask"]["script_args"] and agent.not_supported(added):
return notify_error(
f"Script arguments only available in agent {added} or greater"
)
check = None check = None
if data["autotask"]["assigned_check"]: if data["autotask"]["assigned_check"]:
check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"]) check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
bit_weekdays = None
if data["autotask"]["run_time_days"]:
bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
del data["autotask"]["run_time_days"]
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent) serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
obj = serializer.save( obj = serializer.save(
@@ -56,6 +59,7 @@ class AddAutoTask(APIView):
script=script, script=script,
win_task_name=AutomatedTask.generate_task_name(), win_task_name=AutomatedTask.generate_task_name(),
assigned_check=check, assigned_check=check,
run_time_bit_weekdays=bit_weekdays,
) )
if not "policy" in data: if not "policy" in data:

View File

@@ -36,17 +36,6 @@ class AddCheck(APIView):
else: else:
agent = get_object_or_404(Agent, pk=request.data["pk"]) agent = get_object_or_404(Agent, pk=request.data["pk"])
parent = {"agent": agent} parent = {"agent": agent}
added = "0.11.0"
if (
request.data["check"]["check_type"] == "script"
and request.data["check"]["script_args"]
and agent.not_supported(version_added=added)
):
return notify_error(
{
"non_field_errors": f"Script arguments only available in agent {added} or greater"
}
)
script = None script = None
if "script" in request.data["check"]: if "script" in request.data["check"]:
@@ -58,13 +47,6 @@ class AddCheck(APIView):
request.data["check"]["check_type"] == "eventlog" request.data["check"]["check_type"] == "eventlog"
and request.data["check"]["event_id_is_wildcard"] and request.data["check"]["event_id_is_wildcard"]
): ):
if agent and agent.not_supported(version_added="0.10.2"):
return notify_error(
{
"non_field_errors": "Wildcard is only available in agent 0.10.2 or greater"
}
)
request.data["check"]["event_id"] = 0 request.data["check"]["event_id"] = 0
serializer = CheckSerializer( serializer = CheckSerializer(
@@ -116,31 +98,8 @@ class GetUpdateDeleteCheck(APIView):
pass pass
else: else:
if request.data["event_id_is_wildcard"]: if request.data["event_id_is_wildcard"]:
if check.agent.not_supported(version_added="0.10.2"):
return notify_error(
{
"non_field_errors": "Wildcard is only available in agent 0.10.2 or greater"
}
)
request.data["event_id"] = 0 request.data["event_id"] = 0
elif check.check_type == "script":
added = "0.11.0"
try:
request.data["script_args"]
except KeyError:
pass
else:
if request.data["script_args"] and check.agent.not_supported(
version_added=added
):
return notify_error(
{
"non_field_errors": f"Script arguments only available in agent {added} or greater"
}
)
serializer = CheckSerializer(instance=check, data=request.data, partial=True) serializer = CheckSerializer(instance=check, data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
obj = serializer.save() obj = serializer.save()

View File

@@ -56,8 +56,8 @@ func downloadAgent(filepath string) (err error) {
func main() { func main() {
debugLog := flag.String("log", "", "Verbose output") debugLog := flag.String("log", "", "Verbose output")
localSalt := flag.String("local-salt", "", "Use local salt minion")
localMesh := flag.String("local-mesh", "", "Use local mesh agent") localMesh := flag.String("local-mesh", "", "Use local mesh agent")
noSalt := flag.Bool("nosalt", false, "Does not install salt")
cert := flag.String("cert", "", "Path to ca.pem") cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls") timeout := flag.String("timeout", "", "Timeout for subprocess calls")
flag.Parse() flag.Parse()
@@ -81,8 +81,8 @@ func main() {
cmdArgs = append(cmdArgs, "--log", "DEBUG") cmdArgs = append(cmdArgs, "--log", "DEBUG")
} }
if len(strings.TrimSpace(*localSalt)) != 0 { if *noSalt {
cmdArgs = append(cmdArgs, "--local-salt", *localSalt) cmdArgs = append(cmdArgs, "-nosalt")
} }
if len(strings.TrimSpace(*localMesh)) != 0 { if len(strings.TrimSpace(*localMesh)) != 0 {

View File

@@ -11,12 +11,11 @@ class Command(BaseCommand):
help = "Sets up initial mesh central configuration" help = "Sets up initial mesh central configuration"
async def websocket_call(self, mesh_settings): async def websocket_call(self, mesh_settings):
token = get_auth_token( token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token)
mesh_settings.mesh_username, mesh_settings.mesh_token
)
if settings.MESH_WS_URL: if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" site = mesh_settings.mesh_site.replace("https", "ws")
uri = f"{site}:443/control.ashx?auth={token}"
else: else:
site = mesh_settings.mesh_site.replace("https", "wss") site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}" uri = f"{site}/control.ashx?auth={token}"

View File

@@ -12,12 +12,11 @@ class Command(BaseCommand):
async def websocket_call(self, mesh_settings): async def websocket_call(self, mesh_settings):
token = get_auth_token( token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token)
mesh_settings.mesh_username, mesh_settings.mesh_token
)
if settings.MESH_WS_URL: if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" site = mesh_settings.mesh_site.replace("https", "ws")
uri = f"{site}:443/control.ashx?auth={token}"
else: else:
site = mesh_settings.mesh_site.replace("https", "wss") site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}" uri = f"{site}/control.ashx?auth={token}"
@@ -52,11 +51,17 @@ class Command(BaseCommand):
try: try:
# Check for Mesh Username # Check for Mesh Username
if not mesh_settings.mesh_username or settings.MESH_USERNAME != mesh_settings.mesh_username: if (
not mesh_settings.mesh_username
or settings.MESH_USERNAME != mesh_settings.mesh_username
):
mesh_settings.mesh_username = settings.MESH_USERNAME mesh_settings.mesh_username = settings.MESH_USERNAME
# Check for Mesh Site # Check for Mesh Site
if not mesh_settings.mesh_site or settings.MESH_SITE != mesh_settings.mesh_site: if (
not mesh_settings.mesh_site
or settings.MESH_SITE != mesh_settings.mesh_site
):
mesh_settings.mesh_site = settings.MESH_SITE mesh_settings.mesh_site = settings.MESH_SITE
# Check for Mesh Token # Check for Mesh Token
@@ -75,7 +80,9 @@ class Command(BaseCommand):
return return
try: try:
asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings)) asyncio.get_event_loop().run_until_complete(
self.websocket_call(mesh_settings)
)
self.stdout.write("Initial Mesh Central setup complete") self.stdout.write("Initial Mesh Central setup complete")
except websockets.exceptions.ConnectionClosedError: except websockets.exceptions.ConnectionClosedError:
self.stdout.write( self.stdout.write(

View File

@@ -1,9 +1,7 @@
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
import tempfile import tempfile
from time import sleep
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@@ -15,18 +13,6 @@ class Command(BaseCommand):
help = "Collection of tasks to run after updating the rmm, after migrations" help = "Collection of tasks to run after updating the rmm, after migrations"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
if not os.path.exists("/usr/local/bin/goversioninfo"):
self.stdout.write(self.style.ERROR("*" * 100))
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"ERROR: New update script available. Delete this one and re-download."
)
)
self.stdout.write("\n")
sys.exit(1)
# 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
@@ -43,88 +29,17 @@ class Command(BaseCommand):
self.style.SUCCESS(f"Migrated disks on {agent.hostname}") self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
) )
# sync modules. split into chunks of 60 agents to not overload the salt master
agents = Agent.objects.all()
online = [i.salt_id for i in agents if i.status == "online"]
chunks = (online[i : i + 60] for i in range(0, len(online), 60))
self.stdout.write(self.style.SUCCESS("Syncing agent modules..."))
for chunk in chunks:
r = Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
sleep(5)
has_old_config = True
rmm_conf = "/etc/nginx/sites-available/rmm.conf"
if os.path.exists(rmm_conf):
with open(rmm_conf) as f:
for line in f:
if "location" and "builtin" in line:
has_old_config = False
break
if has_old_config:
new_conf = """
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://rmm.yourwebsite.com";
alias /srv/salt/scripts/;
}
"""
after_this = """
location /saltscripts/ {
internal;
add_header "Access-Control-Allow-Origin" "https://rmm.yourwebsite.com";
alias /srv/salt/scripts/userdefined/;
}
"""
self.stdout.write(self.style.ERROR("*" * 100))
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"WARNING: A recent update requires you to manually edit your nginx config"
)
)
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR("Please add the following location block to ")
+ self.style.WARNING(rmm_conf)
)
self.stdout.write(self.style.SUCCESS(new_conf))
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"You can paste the above right after the following block that's already in your nginx config:"
)
)
self.stdout.write(after_this)
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"Make sure to replace rmm.yourwebsite.com with your domain"
)
)
self.stdout.write(
self.style.ERROR("After editing, restart nginx with the command ")
+ self.style.WARNING("sudo systemctl restart nginx")
)
self.stdout.write("\n")
self.stdout.write(self.style.ERROR("*" * 100))
input("Press Enter to continue...")
# install go # install go
if not os.path.exists("/usr/local/rmmgo/"): if not os.path.exists("/usr/local/rmmgo/"):
self.stdout.write(self.style.SUCCESS("Installing golang")) self.stdout.write(self.style.SUCCESS("Installing golang"))
subprocess.run("sudo mkdir -p /usr/local/rmmgo", shell=True) subprocess.run("sudo mkdir -p /usr/local/rmmgo", shell=True)
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
r = subprocess.run( r = subprocess.run(
f"wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P {tmpdir}", f"wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P {tmpdir}",
shell=True, shell=True,
) )
gotar = os.path.join(tmpdir, "go1.15.linux-amd64.tar.gz") gotar = os.path.join(tmpdir, "go1.15.5.linux-amd64.tar.gz")
subprocess.run(f"tar -xzf {gotar} -C {tmpdir}", shell=True) subprocess.run(f"tar -xzf {gotar} -C {tmpdir}", shell=True)

View File

@@ -1,5 +1,7 @@
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from core.tasks import core_maintenance_tasks from core.tasks import core_maintenance_tasks
from unittest.mock import patch
from model_bakery import baker, seq
class TestCoreTasks(TacticalTestCase): class TestCoreTasks(TacticalTestCase):
@@ -31,3 +33,45 @@ class TestCoreTasks(TacticalTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
def test_ui_maintenance_actions(self, remove_orphaned_win_tasks):
url = "/core/servermaintenance/"
agents = baker.make_recipe("agents.online_agent", _quantity=3)
# test with empty data
r = self.client.post(url, {})
self.assertEqual(r.status_code, 400)
# test with invalid action
data = {"action": "invalid_action"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 400)
# test reload nats action
data = {"action": "reload_nats"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
# test prune db with no tables
data = {"action": "prune_db"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 400)
# test prune db with tables
data = {
"action": "prune_db",
"prune_tables": ["audit_logs", "agent_outages", "pending_actions"],
}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
# test remove orphaned tasks
data = {"action": "rm_orphaned_tasks"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
remove_orphaned_win_tasks.assert_called()
self.check_not_authenticated("post", url)

View File

@@ -8,4 +8,5 @@ urlpatterns = [
path("version/", views.version), path("version/", views.version),
path("emailtest/", views.email_test), path("emailtest/", views.email_test),
path("dashinfo/", views.dashboard_info), path("dashinfo/", views.dashboard_info),
path("servermaintenance/", views.server_maintenance),
] ]

View File

@@ -84,3 +84,56 @@ def email_test(request):
return notify_error(r) return notify_error(r)
return Response("Email Test OK!") return Response("Email Test OK!")
@api_view(["POST"])
def server_maintenance(request):
from tacticalrmm.utils import reload_nats
if "action" not in request.data:
return notify_error("The data is incorrect")
if request.data["action"] == "reload_nats":
reload_nats()
return Response("Nats configuration was reloaded successfully.")
if request.data["action"] == "rm_orphaned_tasks":
from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks
agents = Agent.objects.all()
online = [i for i in agents if i.status == "online"]
for agent in online:
remove_orphaned_win_tasks.delay(agent.pk)
return Response(
"The task has been initiated. Check the Debug Log in the UI for progress."
)
if request.data["action"] == "prune_db":
from agents.models import AgentOutage
from logs.models import AuditLog, PendingAction
if "prune_tables" not in request.data:
return notify_error("The data is incorrect.")
tables = request.data["prune_tables"]
records_count = 0
if "agent_outages" in tables:
agentoutages = AgentOutage.objects.exclude(recovery_time=None)
records_count += agentoutages.count()
agentoutages.delete()
if "audit_logs" in tables:
auditlogs = AuditLog.objects.filter(action="check_run")
records_count += auditlogs.count()
auditlogs.delete()
if "pending_actions" in tables:
pendingactions = PendingAction.objects.filter(status="completed")
records_count += pendingactions.count()
pendingactions.delete()
return Response(f"{records_count} records were pruned from the database")
return notify_error("The data is incorrect")

View File

@@ -1,5 +1,4 @@
import datetime as dt import datetime as dt
import json
from abc import abstractmethod from abc import abstractmethod
from django.db import models from django.db import models
from tacticalrmm.middleware import get_username, get_debug_info from tacticalrmm.middleware import get_username, get_debug_info

View File

@@ -1,29 +0,0 @@
from loguru import logger
from tacticalrmm.celery import app
from django.conf import settings
logger.configure(**settings.LOG_CONFIG)
@app.task
def cancel_pending_action_task(data):
if data["action_type"] == "schedreboot" and data["status"] == "pending":
from agents.models import Agent
agent = Agent.objects.get(pk=data["agent"])
task_name = data["details"]["taskname"]
r = agent.salt_api_cmd(
timeout=30, func="task.delete_task", arg=[f"name={task_name}"]
)
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
logger.error(
f"Unable to contact {agent.hostname}. Task {task_name} will need to cancelled manually."
)
return
else:
logger.info(f"Scheduled reboot cancelled on {agent.hostname}")
return "ok"

View File

@@ -122,10 +122,25 @@ class TestAuditViews(TacticalTestCase):
{"filter": {"clientFilter": [site.client.id]}, "count": 23}, {"filter": {"clientFilter": [site.client.id]}, "count": 23},
] ]
pagination = {
"rowsPerPage": 25,
"page": 1,
"sortBy": "entry_time",
"descending": True,
}
for req in data: for req in data:
resp = self.client.patch(url, req["filter"], format="json") resp = self.client.patch(
url, {**req["filter"], "pagination": pagination}, format="json"
)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), req["count"]) self.assertEqual(
len(resp.data["audit_logs"]),
pagination["rowsPerPage"]
if req["count"] > pagination["rowsPerPage"]
else req["count"],
)
self.assertEqual(resp.data["total"], req["count"])
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)
@@ -190,54 +205,31 @@ class TestAuditViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("logs.tasks.cancel_pending_action_task.delay") @patch("agents.models.Agent.nats_cmd")
def test_cancel_pending_action(self, mock_task): def test_cancel_pending_action(self, nats_cmd):
url = "/logs/cancelpendingaction/" url = "/logs/cancelpendingaction/"
pending_action = baker.make("logs.PendingAction") # TODO fix this TypeError: Object of type coroutine is not JSON serializable
""" agent = baker.make("agents.Agent", version="1.1.1")
pending_action = baker.make(
"logs.PendingAction",
agent=agent,
details={
"time": "2021-01-13 18:20:00",
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
},
)
serializer = PendingActionSerializer(pending_action).data
data = {"pk": pending_action.id} data = {"pk": pending_action.id}
resp = self.client.delete(url, data, format="json") resp = self.client.delete(url, data, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
mock_task.assert_called_with(serializer) nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
}
nats_cmd.assert_called_with(nats_data, timeout=10)
# try request again and it should fail since pending action doesn't exist # try request again and it should fail since pending action doesn't exist
resp = self.client.delete(url, data, format="json") resp = self.client.delete(url, data, format="json")
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404) """
self.check_not_authenticated("delete", url) self.check_not_authenticated("delete", url)
class TestLogsTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
@patch("agents.models.Agent.salt_api_cmd")
def test_cancel_pending_action_task(self, mock_salt_cmd):
from .tasks import cancel_pending_action_task
pending_action = baker.make(
"logs.PendingAction",
action_type="schedreboot",
status="pending",
details={"taskname": "test_name"},
)
# data that is passed to the task
data = PendingActionSerializer(pending_action).data
# set return value on mock to success
mock_salt_cmd.return_value = "success"
# call task with valid data and see if salt is called with correct data
ret = cancel_pending_action_task(data)
mock_salt_cmd.assert_called_with(
timeout=30, func="task.delete_task", arg=["name=test_name"]
)
# this should return successful
self.assertEquals(ret, "ok")
# this run should return false
mock_salt_cmd.reset_mock()
mock_salt_cmd.return_value = "timeout"
ret = cancel_pending_action_task(data)
self.assertEquals(ret, None)

View File

@@ -1,3 +1,4 @@
import asyncio
import subprocess import subprocess
from django.conf import settings from django.conf import settings
@@ -5,6 +6,7 @@ from django.shortcuts import get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from django.db.models import Q from django.db.models import Q
from django.core.paginator import Paginator
from datetime import datetime as dt from datetime import datetime as dt
from rest_framework.response import Response from rest_framework.response import Response
@@ -18,7 +20,7 @@ from accounts.models import User
from .serializers import PendingActionSerializer, AuditLogSerializer from .serializers import PendingActionSerializer, AuditLogSerializer
from agents.serializers import AgentHostnameSerializer from agents.serializers import AgentHostnameSerializer
from accounts.serializers import UserSerializer from accounts.serializers import UserSerializer
from .tasks import cancel_pending_action_task from tacticalrmm.utils import notify_error
class GetAuditLogs(APIView): class GetAuditLogs(APIView):
@@ -26,6 +28,14 @@ class GetAuditLogs(APIView):
from clients.models import Client from clients.models import Client
from agents.models import Agent from agents.models import Agent
pagination = request.data["pagination"]
order_by = (
f"-{pagination['sortBy']}"
if pagination["descending"]
else f"{pagination['sortBy']}"
)
agentFilter = Q() agentFilter = Q()
clientFilter = Q() clientFilter = Q()
actionFilter = Q() actionFilter = Q()
@@ -67,9 +77,18 @@ class GetAuditLogs(APIView):
.filter(actionFilter) .filter(actionFilter)
.filter(objectFilter) .filter(objectFilter)
.filter(timeFilter) .filter(timeFilter)
) ).order_by(order_by)
return Response(AuditLogSerializer(audit_logs, many=True).data) paginator = Paginator(audit_logs, pagination["rowsPerPage"])
return Response(
{
"audit_logs": AuditLogSerializer(
paginator.get_page(pagination["page"]), many=True
).data,
"total": paginator.count,
}
)
class FilterOptionsAuditLog(APIView): class FilterOptionsAuditLog(APIView):
@@ -95,19 +114,26 @@ def agent_pending_actions(request, pk):
@api_view() @api_view()
def all_pending_actions(request): def all_pending_actions(request):
actions = PendingAction.objects.all() actions = PendingAction.objects.all().select_related("agent")
return Response(PendingActionSerializer(actions, many=True).data) return Response(PendingActionSerializer(actions, many=True).data)
@api_view(["DELETE"]) @api_view(["DELETE"])
def cancel_pending_action(request): def cancel_pending_action(request):
action = get_object_or_404(PendingAction, pk=request.data["pk"]) action = get_object_or_404(PendingAction, pk=request.data["pk"])
data = PendingActionSerializer(action).data if not action.agent.has_gotasks:
cancel_pending_action_task.delay(data) return notify_error("Requires agent version 1.1.1 or greater")
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": action.details["taskname"]},
}
r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
action.delete() action.delete()
return Response( return Response(f"{action.agent.hostname}: {action.description} was cancelled")
f"{action.agent.hostname}: {action.description} will be cancelled shortly"
)
@api_view() @api_view()

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import string
from time import sleep from time import sleep
from loguru import logger from loguru import logger
from tacticalrmm.celery import app from tacticalrmm.celery import app
@@ -8,6 +7,7 @@ from django.utils import timezone as djangotime
from agents.models import Agent from agents.models import Agent
from .models import ChocoSoftware, ChocoLog, InstalledSoftware from .models import ChocoSoftware, ChocoLog, InstalledSoftware
from tacticalrmm.utils import filter_software
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
@@ -87,44 +87,6 @@ def update_chocos():
return "ok" return "ok"
@app.task
def get_installed_software(pk):
agent = Agent.objects.get(pk=pk)
if not agent.has_nats:
logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0")
return
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=20))
if r == "timeout" or r == "natsdown":
logger.error(f"{agent.salt_id} {r}")
return
printable = set(string.printable)
sw = []
for s in r:
sw.append(
{
"name": "".join(filter(lambda x: x in printable, s["name"])),
"version": "".join(filter(lambda x: x in printable, s["version"])),
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
"install_date": s["install_date"],
"size": s["size"],
"source": s["source"],
"location": s["location"],
"uninstall": s["uninstall"],
}
)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return "ok"
@app.task @app.task
def install_program(pk, name, version): def install_program(pk, name, version):
agent = Agent.objects.get(pk=pk) agent = Agent.objects.get(pk=pk)
@@ -169,6 +131,4 @@ def install_program(pk, name, version):
agent=agent, name=name, version=version, message=output, installed=installed agent=agent, name=name, version=version, message=output, installed=installed
).save() ).save()
get_installed_software.delay(agent.pk)
return "ok" return "ok"

View File

@@ -120,61 +120,8 @@ class TestSoftwareTasks(TacticalTestCase):
salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list") salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list")
self.assertEquals(salt_api_cmd.call_count, 2) self.assertEquals(salt_api_cmd.call_count, 2)
@patch("agents.models.Agent.nats_cmd")
def test_get_installed_software(self, nats_cmd):
from .tasks import get_installed_software
agent = baker.make_recipe("agents.agent")
nats_return = [
{
"name": "Mozilla Maintenance Service",
"size": "336.9 kB",
"source": "",
"version": "73.0.1",
"location": "",
"publisher": "Mozilla",
"uninstall": '"C:\\Program Files (x86)\\Mozilla Maintenance Service\\uninstall.exe"',
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
{
"name": "OpenVPN 2.4.9-I601-Win10 ",
"size": "8.7 MB",
"source": "",
"version": "2.4.9-I601-Win10",
"location": "C:\\Program Files\\OpenVPN\\",
"publisher": "OpenVPN Technologies, Inc.",
"uninstall": "C:\\Program Files\\OpenVPN\\Uninstall.exe",
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
{
"name": "Microsoft Office Professional Plus 2019 - en-us",
"size": "0 B",
"source": "",
"version": "16.0.10368.20035",
"location": "C:\\Program Files\\Microsoft Office",
"publisher": "Microsoft Corporation",
"uninstall": '"C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeClickToRun.exe" scenario=install scenariosubtype=ARP sourcetype=None productstoremove=ProPlus2019Volume.16_en-us_x-none culture=en-us version.16=16.0',
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
]
# test failed attempt
nats_cmd.return_value = "timeout"
ret = get_installed_software(agent.pk)
self.assertFalse(ret)
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
nats_cmd.reset_mock()
# test successful attempt
nats_cmd.return_value = nats_return
ret = get_installed_software(agent.pk)
self.assertTrue(ret)
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.salt_api_cmd")
@patch("software.tasks.get_installed_software.delay") def test_install_program(self, salt_api_cmd):
def test_install_program(self, get_installed_software, salt_api_cmd):
from .tasks import install_program from .tasks import install_program
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")
@@ -195,6 +142,5 @@ class TestSoftwareTasks(TacticalTestCase):
salt_api_cmd.assert_called_with( salt_api_cmd.assert_called_with(
timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"] timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"]
) )
get_installed_software.assert_called_with(agent.pk)
self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists()) self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists())

View File

@@ -1,5 +1,5 @@
import asyncio import asyncio
import string from typing import Any
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -10,7 +10,7 @@ from agents.models import Agent
from .models import ChocoSoftware, InstalledSoftware from .models import ChocoSoftware, InstalledSoftware
from .serializers import InstalledSoftwareSerializer from .serializers import InstalledSoftwareSerializer
from .tasks import install_program from .tasks import install_program
from tacticalrmm.utils import notify_error from tacticalrmm.utils import notify_error, filter_software
@api_view() @api_view()
@@ -45,25 +45,11 @@ def refresh_installed(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")
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown": if r == "timeout" or r == "natsdown":
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
printable = set(string.printable) sw = filter_software(r)
sw = []
for s in r:
sw.append(
{
"name": "".join(filter(lambda x: x in printable, s["name"])),
"version": "".join(filter(lambda x: x in printable, s["version"])),
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
"install_date": s["install_date"],
"size": s["size"],
"source": s["source"],
"location": s["location"],
"uninstall": s["uninstall"],
}
)
if not InstalledSoftware.objects.filter(agent=agent).exists(): if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save() InstalledSoftware(agent=agent, software=sw).save()

View File

@@ -37,14 +37,6 @@ app.conf.beat_schedule = {
"task": "agents.tasks.batch_sync_modules_task", "task": "agents.tasks.batch_sync_modules_task",
"schedule": crontab(minute=25, hour="*/4"), "schedule": crontab(minute=25, hour="*/4"),
}, },
"sys-info": {
"task": "agents.tasks.batch_sysinfo_task",
"schedule": crontab(minute=15, hour="*/2"),
},
"update-salt": {
"task": "agents.tasks.update_salt_minion_task",
"schedule": crontab(minute=20, hour="*/6"),
},
"agent-auto-update": { "agent-auto-update": {
"task": "agents.tasks.auto_self_agent_update_task", "task": "agents.tasks.auto_self_agent_update_task",
"schedule": crontab(minute=35, hour="*"), "schedule": crontab(minute=35, hour="*"),

View File

@@ -15,19 +15,19 @@ 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.2.3" TRMM_VERSION = "0.2.5"
# 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.92" APP_VER = "0.0.95"
# https://github.com/wh1te909/salt # https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.1.0" LATEST_SALT_VER = "1.1.0"
# https://github.com/wh1te909/rmmagent # https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.1.0" LATEST_AGENT_VER = "1.1.1"
MESH_VER = "0.6.84" MESH_VER = "0.7.10"
SALT_MASTER_VER = "3002.2" SALT_MASTER_VER = "3002.2"
@@ -58,7 +58,6 @@ INSTALLED_APPS = [
"knox", "knox",
"corsheaders", "corsheaders",
"accounts", "accounts",
"api",
"apiv2", "apiv2",
"apiv3", "apiv3",
"clients", "clients",
@@ -156,39 +155,6 @@ LOG_CONFIG = {
"handlers": [{"sink": os.path.join(LOG_DIR, "debug.log"), "serialize": False}] "handlers": [{"sink": os.path.join(LOG_DIR, "debug.log"), "serialize": False}]
} }
if "TRAVIS" in os.environ:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "travisci",
"USER": "travisci",
"PASSWORD": "travisSuperSekret6645",
"HOST": "127.0.0.1",
"PORT": "",
}
}
REST_FRAMEWORK = {
"DATETIME_FORMAT": "%b-%d-%Y - %H:%M",
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}
DEBUG = True
SECRET_KEY = "abcdefghijklmnoptravis123456789"
ADMIN_URL = "abc123456/"
SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts")
SALT_USERNAME = "travis"
SALT_PASSWORD = "travis"
MESH_USERNAME = "travis"
MESH_SITE = "https://example.com"
MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
REDIS_HOST = "localhost"
SALT_HOST = "127.0.0.1"
if "AZPIPELINE" in os.environ: if "AZPIPELINE" in os.environ:
DATABASES = { DATABASES = {
"default": { "default": {
@@ -208,6 +174,8 @@ if "AZPIPELINE" in os.environ:
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
} }
ALLOWED_HOSTS = ["api.example.com"]
DOCKER_BUILD = True
DEBUG = True DEBUG = True
SECRET_KEY = "abcdefghijklmnoptravis123456789" SECRET_KEY = "abcdefghijklmnoptravis123456789"

View File

@@ -10,7 +10,6 @@ urlpatterns = [
path("login/", LoginView.as_view()), path("login/", LoginView.as_view()),
path("logout/", knox_views.LogoutView.as_view()), path("logout/", knox_views.LogoutView.as_view()),
path("logoutall/", knox_views.LogoutAllView.as_view()), path("logoutall/", knox_views.LogoutAllView.as_view()),
path("api/v1/", include("api.urls")),
path("api/v2/", include("apiv2.urls")), path("api/v2/", include("apiv2.urls")),
path("api/v3/", include("apiv3.urls")), path("api/v3/", include("apiv3.urls")),
path("clients/", include("clients.urls")), path("clients/", include("clients.urls")),

View File

@@ -1,6 +1,8 @@
import json import json
import os import os
import string
import subprocess import subprocess
from typing import List, Dict
from loguru import logger from loguru import logger
from django.conf import settings from django.conf import settings
@@ -13,6 +15,68 @@ logger.configure(**settings.LOG_CONFIG)
notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST)
SoftwareList = List[Dict[str, str]]
WEEK_DAYS = {
"Sunday": 0x1,
"Monday": 0x2,
"Tuesday": 0x4,
"Wednesday": 0x8,
"Thursday": 0x10,
"Friday": 0x20,
"Saturday": 0x40,
}
def get_bit_days(days: List[str]) -> int:
bit_days = 0
for day in days:
bit_days |= WEEK_DAYS.get(day)
return bit_days
def bitdays_to_string(day: int) -> str:
ret = []
if day == 127:
return "Every day"
if day & WEEK_DAYS["Sunday"]:
ret.append("Sunday")
if day & WEEK_DAYS["Monday"]:
ret.append("Monday")
if day & WEEK_DAYS["Tuesday"]:
ret.append("Tuesday")
if day & WEEK_DAYS["Wednesday"]:
ret.append("Wednesday")
if day & WEEK_DAYS["Thursday"]:
ret.append("Thursday")
if day & WEEK_DAYS["Friday"]:
ret.append("Friday")
if day & WEEK_DAYS["Saturday"]:
ret.append("Saturday")
return ", ".join(ret)
def filter_software(sw: SoftwareList) -> SoftwareList:
ret: SoftwareList = []
printable = set(string.printable)
for s in sw:
ret.append(
{
"name": "".join(filter(lambda x: x in printable, s["name"])),
"version": "".join(filter(lambda x: x in printable, s["version"])),
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
"install_date": s["install_date"],
"size": s["size"],
"source": s["source"],
"location": s["location"],
"uninstall": s["uninstall"],
}
)
return ret
def reload_nats(): def reload_nats():
users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}]
@@ -27,16 +91,22 @@ def reload_nats():
f"{agent.hostname} does not have a user account, NATS will not work" f"{agent.hostname} does not have a user account, NATS will not work"
) )
if not settings.DOCKER_BUILD:
domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1]
cert_path = f"/etc/letsencrypt/live/{domain}" if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"):
if os.path.exists(settings.CERT_FILE) and os.path.exists(settings.KEY_FILE):
cert_file = settings.CERT_FILE
key_file = settings.KEY_FILE
else: else:
cert_path = "/opt/tactical/certs" cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem"
else:
cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem"
config = { config = {
"tls": { "tls": {
"cert_file": f"{cert_path}/fullchain.pem", "cert_file": cert_file,
"key_file": f"{cert_path}/privkey.pem", "key_file": key_file,
}, },
"authorization": {"users": users}, "authorization": {"users": users},
"max_payload": 2048576005, "max_payload": 2048576005,

View File

@@ -175,7 +175,7 @@ class WinupdateTasks(TacticalTestCase):
agent_salt_cmd.assert_called_with(func="win_agent.install_updates") agent_salt_cmd.assert_called_with(func="win_agent.install_updates")
self.assertEquals(agent_salt_cmd.call_count, 2) self.assertEquals(agent_salt_cmd.call_count, 2)
@patch("agents.models.Agent.salt_api_async") """ @patch("agents.models.Agent.salt_api_async")
def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): def test_check_agent_update_monthly_schedule(self, agent_salt_cmd):
from .tasks import check_agent_update_schedule_task from .tasks import check_agent_update_schedule_task
@@ -204,7 +204,7 @@ class WinupdateTasks(TacticalTestCase):
check_agent_update_schedule_task() check_agent_update_schedule_task()
agent_salt_cmd.assert_called_with(func="win_agent.install_updates") agent_salt_cmd.assert_called_with(func="win_agent.install_updates")
self.assertEquals(agent_salt_cmd.call_count, 2) self.assertEquals(agent_salt_cmd.call_count, 2) """
@patch("agents.models.Agent.salt_api_cmd") @patch("agents.models.Agent.salt_api_cmd")
def test_check_for_updates(self, salt_api_cmd): def test_check_for_updates(self, salt_api_cmd):

View File

@@ -19,7 +19,7 @@ FROM nginx:stable-alpine
ENV PUBLIC_DIR /usr/share/nginx/html ENV PUBLIC_DIR /usr/share/nginx/html
RUN apk add --no-cache bash RUN apk add --no-cache bash
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR} COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR}

View File

@@ -6,9 +6,12 @@ ENV TACTICAL_DIR /opt/tactical
RUN apk add --no-cache bash RUN apk add --no-cache bash
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
RUN npm install meshcentral@0.6.62 COPY api/tacticalrmm/tacticalrmm/settings.py /tmp/settings.py
RUN grep -o 'MESH_VER.*' /tmp/settings.py | cut -d'"' -f 2 > /tmp/MESH_VER && \
npm install meshcentral@$(cat /tmp/MESH_VER)
COPY docker/containers/tactical-meshcentral/entrypoint.sh / COPY docker/containers/tactical-meshcentral/entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -5,7 +5,7 @@ ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
RUN apk add --no-cache inotify-tools supervisor bash RUN apk add --no-cache inotify-tools supervisor bash
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY docker/containers/tactical-nats/entrypoint.sh / COPY docker/containers/tactical-nats/entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -4,7 +4,7 @@ ENV TACTICAL_DIR /opt/tactical
RUN apk add --no-cache openssl bash RUN apk add --no-cache openssl bash
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/ COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/entrypoint.sh RUN chmod +x /docker-entrypoint.d/entrypoint.sh

View File

@@ -4,6 +4,8 @@ ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
ENV SALT_USER saltapi ENV SALT_USER saltapi
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \ apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \
wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \ wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \

View File

@@ -69,6 +69,9 @@ DEBUG = False
DOCKER_BUILD = True DOCKER_BUILD = True
CERT_FILE = '/opt/tactical/certs/fullchain.pem'
CERT_KEY = '/opt/tactical/certs/privkey.pem'
SCRIPTS_DIR = '/opt/tactical/scripts' SCRIPTS_DIR = '/opt/tactical/scripts'
ALLOWED_HOSTS = ['${API_HOST}'] ALLOWED_HOSTS = ['${API_HOST}']

View File

@@ -101,8 +101,10 @@ services:
MONGODB_USER: ${MONGODB_USER} MONGODB_USER: ${MONGODB_USER}
MONGODB_PASSWORD: ${MONGODB_PASSWORD} MONGODB_PASSWORD: ${MONGODB_PASSWORD}
networks: networks:
- proxy proxy:
- mesh-db aliases:
- ${MESH_HOST}
mesh-db:
volumes: volumes:
- tactical_data:/opt/tactical - tactical_data:/opt/tactical
- mesh_data:/home/node/app/meshcentral-data - mesh_data:/home/node/app/meshcentral-data

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
SCRIPT_VERSION="23" SCRIPT_VERSION="26"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh' SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -205,12 +205,47 @@ sudo apt install -y mongodb-org
sudo systemctl enable mongod sudo systemctl enable mongod
sudo systemctl restart mongod sudo systemctl restart mongod
print_green 'Installing python, redis and git'
sudo apt update
sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git
print_green 'Installing postgresql'
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
print_green 'Creating database for the rmm'
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo mkdir /rmm
sudo chown ${USER}:${USER} /rmm
sudo mkdir -p /var/log/celery
sudo chown ${USER}:${USER} /var/log/celery
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
cd /rmm
git config user.email "admin@example.com"
git config user.name "Bob"
git checkout master
print_green 'Installing MeshCentral' print_green 'Installing MeshCentral'
MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
sudo mkdir -p /meshcentral/meshcentral-data sudo mkdir -p /meshcentral/meshcentral-data
sudo chown ${USER}:${USER} -R /meshcentral sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral cd /meshcentral
npm install meshcentral@0.6.84 npm install meshcentral@${MESH_VER}
sudo chown ${USER}:${USER} -R /meshcentral sudo chown ${USER}:${USER} -R /meshcentral
meshcfg="$(cat << EOF meshcfg="$(cat << EOF
@@ -253,37 +288,6 @@ EOF
)" )"
echo "${meshcfg}" > /meshcentral/meshcentral-data/config.json echo "${meshcfg}" > /meshcentral/meshcentral-data/config.json
print_green 'Installing python, redis and git'
sudo apt update
sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git
print_green 'Installing postgresql'
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
print_green 'Creating database for the rmm'
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo mkdir /rmm
sudo chown ${USER}:${USER} /rmm
sudo mkdir -p /var/log/celery
sudo chown ${USER}:${USER} /var/log/celery
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
cd /rmm
git config user.email "admin@example.com"
git config user.name "Bob"
git checkout master
localvars="$(cat << EOF localvars="$(cat << EOF
SECRET_KEY = "${DJANGO_SEKRET}" SECRET_KEY = "${DJANGO_SEKRET}"
@@ -504,14 +508,7 @@ nginxmesh="$(cat << EOF
server { server {
listen 80; listen 80;
server_name ${meshdomain}; server_name ${meshdomain};
location / { return 301 https://\$server_name\$request_uri;
proxy_pass http://127.0.0.1:800;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host \$host:\$server_port;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
} }
server { server {
@@ -530,6 +527,7 @@ server {
proxy_pass http://127.0.0.1:4430/; proxy_pass http://127.0.0.1:4430/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header Upgrade \$http_upgrade; proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host \$host:\$server_port; proxy_set_header X-Forwarded-Host \$host:\$server_port;
@@ -557,7 +555,6 @@ sleep 30
saltvars="$(cat << EOF saltvars="$(cat << EOF
timeout: 20 timeout: 20
worker_threads: 15
gather_job_timeout: 25 gather_job_timeout: 25
max_event_size: 30485760 max_event_size: 30485760
external_auth: external_auth:
@@ -572,8 +569,6 @@ rest_cherrypy:
port: 8123 port: 8123
disable_ssl: True disable_ssl: True
max_request_body_size: 30485760 max_request_body_size: 30485760
thread_pool: 300
socket_queue_size: 100
EOF EOF
)" )"

View File

@@ -7,7 +7,7 @@ pgpw="hunter2"
##################################################### #####################################################
SCRIPT_VERSION="7" SCRIPT_VERSION="9"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh' SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -207,15 +207,6 @@ sudo systemctl restart mongod
sleep 5 sleep 5
mongorestore --gzip $tmp_dir/meshcentral/mongo mongorestore --gzip $tmp_dir/meshcentral/mongo
print_green 'Restoring MeshCentral'
sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C /
sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral
npm install meshcentral@0.6.84
print_green 'Restoring the backend'
sudo mkdir /rmm sudo mkdir /rmm
sudo chown ${USER}:${USER} /rmm sudo chown ${USER}:${USER} /rmm
@@ -227,6 +218,17 @@ git config user.email "admin@example.com"
git config user.name "Bob" git config user.name "Bob"
git checkout master git checkout master
print_green 'Restoring MeshCentral'
MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}')
sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C /
sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral
npm install meshcentral@${MESH_VER}
print_green 'Restoring the backend'
cp $tmp_dir/rmm/local_settings.py /rmm/api/tacticalrmm/tacticalrmm/ cp $tmp_dir/rmm/local_settings.py /rmm/api/tacticalrmm/tacticalrmm/
cp $tmp_dir/rmm/env /rmm/web/.env cp $tmp_dir/rmm/env /rmm/web/.env
cp $tmp_dir/rmm/app.ini /rmm/api/tacticalrmm/ cp $tmp_dir/rmm/app.ini /rmm/api/tacticalrmm/

View File

@@ -46,6 +46,13 @@
</q-icon> </q-icon>
</q-th> </q-th>
</template> </template>
<template v-slot:header-cell-pendingactions="props">
<q-th auto-width :props="props">
<q-icon name="far fa-clock" size="1.5em">
<q-tooltip>Pending Actions</q-tooltip>
</q-icon>
</q-th>
</template>
<!-- <!--
<template v-slot:header-cell-antivirus="props"> <template v-slot:header-cell-antivirus="props">
<q-th auto-width :props="props"> <q-th auto-width :props="props">
@@ -101,14 +108,6 @@
<q-item-section>Take Control</q-item-section> <q-item-section>Take Control</q-item-section>
</q-item> </q-item>
<!-- web rdp -->
<q-item clickable v-ripple v-close-popup @click.stop.prevent="webRDP(props.row.id)">
<q-item-section side>
<q-icon size="xs" name="screen_share" />
</q-item-section>
<q-item-section>Remote Desktop</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showSendCommand = true"> <q-item clickable v-ripple v-close-popup @click="showSendCommand = true">
<q-item-section side> <q-item-section side>
<q-icon size="xs" name="fas fa-terminal" /> <q-icon size="xs" name="fas fa-terminal" />
@@ -274,6 +273,18 @@
<q-tooltip>Patches Pending</q-tooltip> <q-tooltip>Patches Pending</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
<q-td :props="props" key="pendingactions">
<q-icon
v-if="props.row.pending_actions !== 0"
@click="showPendingActionsModal(props.row.id)"
name="far fa-clock"
size="1.4em"
color="warning"
class="cursor-pointer"
>
<q-tooltip>Pending Action Count: {{ props.row.pending_actions }}</q-tooltip>
</q-icon>
</q-td>
<q-td key="agentstatus"> <q-td key="agentstatus">
<q-icon v-if="props.row.status === 'overdue'" name="fas fa-signal" size="1.2em" color="negative"> <q-icon v-if="props.row.status === 'overdue'" name="fas fa-signal" size="1.2em" color="negative">
<q-tooltip>Agent overdue</q-tooltip> <q-tooltip>Agent overdue</q-tooltip>
@@ -305,12 +316,12 @@
</q-dialog> </q-dialog>
<!-- reboot later modal --> <!-- reboot later modal -->
<q-dialog v-model="showRebootLaterModal"> <q-dialog v-model="showRebootLaterModal">
<RebootLater @close="showRebootLaterModal = false" /> <RebootLater @close="showRebootLaterModal = false" @edited="agentEdited" />
</q-dialog> </q-dialog>
<!-- pending actions modal --> <!-- pending actions modal -->
<div class="q-pa-md q-gutter-sm"> <div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showPendingActions" @hide="closePendingActionsModal"> <q-dialog v-model="showPendingActions" @hide="closePendingActionsModal">
<PendingActions :agentpk="pendingActionAgentPk" @close="closePendingActionsModal" /> <PendingActions :agentpk="pendingActionAgentPk" @close="closePendingActionsModal" @edited="agentEdited" />
</q-dialog> </q-dialog>
</div> </div>
<!-- add policy modal --> <!-- add policy modal -->
@@ -384,6 +395,7 @@ export default {
let availability = null; let availability = null;
let checks = false; let checks = false;
let patches = false; let patches = false;
let actions = false;
let reboot = false; let reboot = false;
let search = ""; let search = "";
@@ -394,6 +406,7 @@ export default {
advancedFilter = true; advancedFilter = true;
let filter = param.split(":")[1]; let filter = param.split(":")[1];
if (filter === "patchespending") patches = true; if (filter === "patchespending") patches = true;
if (filter === "actionspending") actions = true;
else if (filter === "checksfailing") checks = true; else if (filter === "checksfailing") checks = true;
else if (filter === "rebootneeded") reboot = true; else if (filter === "rebootneeded") reboot = true;
else if (filter === "online" || filter === "offline" || filter === "expired") availability = filter; else if (filter === "online" || filter === "offline" || filter === "expired") availability = filter;
@@ -406,6 +419,7 @@ export default {
if (advancedFilter) { if (advancedFilter) {
if (checks && !row.checks.has_failing_checks) return false; if (checks && !row.checks.has_failing_checks) return false;
if (patches && !row.patches_pending) return false; if (patches && !row.patches_pending) return false;
if (actions && row.pending_actions > 0) return false;
if (reboot && !row.needs_reboot) return false; if (reboot && !row.needs_reboot) return false;
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;
@@ -548,12 +562,16 @@ export default {
persistent: true, persistent: true,
}) })
.onOk(() => { .onOk(() => {
const data = { pk: pk, action: "rebootnow" }; this.$q.loading.show();
axios.post("/agents/poweraction/", data).then(r => { this.$axios
this.$q.dialog({ .post("/agents/reboot/", { pk: pk })
title: `Restarting ${hostname}`, .then(r => {
message: `${hostname} will now be restarted`, this.$q.loading.hide();
}); this.notifySuccess(`${hostname} will now be restarted`);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
}); });
}); });
}, },
@@ -598,19 +616,6 @@ export default {
this.policyAddPk = pk; this.policyAddPk = pk;
this.showPolicyAddModal = true; this.showPolicyAddModal = true;
}, },
webRDP(pk) {
this.$q.loading.show();
this.$axios
.get(`/agents/${pk}/meshcentral/`)
.then(r => {
this.$q.loading.hide();
openURL(r.data.webrdp);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
});
},
toggleMaintenance(agent) { toggleMaintenance(agent) {
let data = { let data = {
id: agent.id, id: agent.id,

View File

@@ -119,6 +119,7 @@
<q-separator /> <q-separator />
<q-card-section> <q-card-section>
<q-table <q-table
@request="onRequest"
dense dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }" :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="audit-mgr-tbl-sticky" class="audit-mgr-tbl-sticky"
@@ -131,6 +132,7 @@
:rows-per-page-options="[25, 50, 100, 500, 1000]" :rows-per-page-options="[25, 50, 100, 500, 1000]"
:no-data-label="noDataText" :no-data-label="noDataText"
@row-click="showDetails" @row-click="showDetails"
virtual-scroll
> >
<template v-slot:top-right> <template v-slot:top-right>
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportLog" /> <q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportLog" />
@@ -263,8 +265,10 @@ export default {
], ],
pagination: { pagination: {
rowsPerPage: 25, rowsPerPage: 25,
rowsNumber: null,
sortBy: "entry_time", sortBy: "entry_time",
descending: true, descending: true,
page: 1,
}, },
}; };
}, },
@@ -355,10 +359,23 @@ export default {
}); });
} }
}, },
onRequest(props) {
// needed to update external pagination object
const { page, rowsPerPage, sortBy, descending } = props.pagination;
this.pagination.page = page;
this.pagination.rowsPerPage = rowsPerPage;
this.pagination.sortBy = sortBy;
this.pagination.descending = descending;
this.search();
},
search() { search() {
this.$q.loading.show(); this.$q.loading.show();
this.searched = true; this.searched = true;
let data = {}; let data = {
pagination: this.pagination,
};
if (!!this.agentFilter && this.agentFilter.length > 0) data["agentFilter"] = this.agentFilter; if (!!this.agentFilter && this.agentFilter.length > 0) data["agentFilter"] = this.agentFilter;
else if (!!this.clientFilter && this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter; else if (!!this.clientFilter && this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter;
@@ -371,7 +388,8 @@ export default {
.patch("/logs/auditlogs/", data) .patch("/logs/auditlogs/", data)
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();
this.auditLogs = Object.freeze(r.data); this.auditLogs = Object.freeze(r.data.audit_logs);
this.pagination.rowsNumber = r.data.total;
}) })
.catch(e => { .catch(e => {
this.$q.loading.hide(); this.$q.loading.hide();

View File

@@ -129,7 +129,7 @@
dense dense
@input="checkAlert(props.row.id, 'Text', props.row.text_alert, props.row.managed_by_policy)" @input="checkAlert(props.row.id, 'Text', props.row.text_alert, props.row.managed_by_policy)"
v-model="props.row.text_alert" v-model="props.row.text_alert"
:disabled="props.row.managed_by_policy" :disable="props.row.managed_by_policy"
/> />
</q-td> </q-td>
<!-- email alert --> <!-- email alert -->
@@ -138,7 +138,7 @@
dense dense
@input="checkAlert(props.row.id, 'Email', props.row.email_alert, props.row.managed_by_policy)" @input="checkAlert(props.row.id, 'Email', props.row.email_alert, props.row.managed_by_policy)"
v-model="props.row.email_alert" v-model="props.row.email_alert"
:disabled="props.row.managed_by_policy" :disable="props.row.managed_by_policy"
/> />
</q-td> </q-td>
<!-- policy check icon --> <!-- policy check icon -->

View File

@@ -130,6 +130,10 @@
<q-item clickable v-close-popup @click="showBulkActionModal('scan')"> <q-item clickable v-close-popup @click="showBulkActionModal('scan')">
<q-item-section>Bulk Patch Management</q-item-section> <q-item-section>Bulk Patch Management</q-item-section>
</q-item> </q-item>
<!-- server maintenance -->
<q-item clickable v-close-popup @click="showServerMaintenance = true">
<q-item-section>Server Maintenance</q-item-section>
</q-item>
</q-list> </q-list>
</q-menu> </q-menu>
</q-btn> </q-btn>
@@ -174,7 +178,7 @@
<!-- Update Agents Modal --> <!-- Update Agents Modal -->
<div class="q-pa-md q-gutter-sm"> <div class="q-pa-md q-gutter-sm">
<q-dialog v-model="showUpdateAgentsModal" maximized transition-show="slide-up" transition-hide="slide-down"> <q-dialog v-model="showUpdateAgentsModal" maximized transition-show="slide-up" transition-hide="slide-down">
<UpdateAgents @close="showUpdateAgentsModal = false" /> <UpdateAgents @close="showUpdateAgentsModal = false" @edited="edited" />
</q-dialog> </q-dialog>
</div> </div>
<!-- Script Manager --> <!-- Script Manager -->
@@ -196,16 +200,18 @@
<q-dialog v-model="showUploadMesh"> <q-dialog v-model="showUploadMesh">
<UploadMesh @close="showUploadMesh = false" /> <UploadMesh @close="showUploadMesh = false" />
</q-dialog> </q-dialog>
<!-- Bulk action modal --> <!-- Bulk action modal -->
<q-dialog v-model="showBulkAction" @hide="closeBulkActionModal" position="top"> <q-dialog v-model="showBulkAction" @hide="closeBulkActionModal" position="top">
<BulkAction :mode="bulkMode" @close="closeBulkActionModal" /> <BulkAction :mode="bulkMode" @close="closeBulkActionModal" />
</q-dialog> </q-dialog>
<!-- Agent Deployment --> <!-- Agent Deployment -->
<q-dialog v-model="showDeployment"> <q-dialog v-model="showDeployment">
<Deployment @close="showDeployment = false" /> <Deployment @close="showDeployment = false" />
</q-dialog> </q-dialog>
<!-- Server Maintenance -->
<q-dialog v-model="showServerMaintenance">
<ServerMaintenance @close="showMaintenance = false" />
</q-dialog>
</q-bar> </q-bar>
</div> </div>
</template> </template>
@@ -225,6 +231,7 @@ import UploadMesh from "@/components/modals/core/UploadMesh";
import AuditManager from "@/components/AuditManager"; import AuditManager from "@/components/AuditManager";
import BulkAction from "@/components/modals/agents/BulkAction"; import BulkAction from "@/components/modals/agents/BulkAction";
import Deployment from "@/components/Deployment"; import Deployment from "@/components/Deployment";
import ServerMaintenance from "@/components/modals/core/ServerMaintenance";
export default { export default {
name: "FileBar", name: "FileBar",
@@ -243,10 +250,12 @@ export default {
AuditManager, AuditManager,
BulkAction, BulkAction,
Deployment, Deployment,
ServerMaintenance,
}, },
props: ["clients"], props: ["clients"],
data() { data() {
return { return {
showServerMaintenance: false,
showClientFormModal: false, showClientFormModal: false,
showSiteFormModal: false, showSiteFormModal: false,
clientOp: null, clientOp: null,

View File

@@ -1,6 +1,5 @@
<template> <template>
<div v-if="!this.selectedAgentPk">No agent selected</div> <div v-if="!this.selectedAgentPk">No agent selected</div>
<div v-else-if="!Array.isArray(software) || !software.length">No software</div>
<div v-else> <div v-else>
<div class="row q-pt-xs items-start"> <div class="row q-pt-xs items-start">
<q-btn <q-btn

View File

@@ -32,13 +32,9 @@
</div> </div>
<div class="q-pa-xs q-gutter-xs"> <div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> <q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--local-salt "C:\\&lt;some folder or path&gt;\\salt-minion-setup.exe"</code> <code>--nosalt</code>
</q-badge> </q-badge>
<span> <span> Do not install salt during agent install. </span>
To skip downloading the salt-minion during the install. Download it
<a v-if="info.arch === '64'" :href="info.data.salt64">here</a>
<a v-else :href="info.data.salt32">here</a>
</span>
</div> </div>
<div class="q-pa-xs q-gutter-xs"> <div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> <q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
@@ -57,12 +53,6 @@
</q-badge> </q-badge>
<span> To use a domain CA </span> <span> To use a domain CA </span>
</div> </div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>--timeout NUMBER_IN_SECONDS</code>
</q-badge>
<span> To increase the default timeout of 900 seconds for the installer. Use on slow computers.</span>
</div>
</q-expansion-item> </q-expansion-item>
<br /> <br />
<p class="text-italic">Note: the auth token above will be valid for {{ info.expires }} hours.</p> <p class="text-italic">Note: the auth token above will be valid for {{ info.expires }} hours.</p>

View File

@@ -74,7 +74,7 @@
<q-select outlined dense options-dense v-model="timezone" :options="allTimezones" class="col-8" /> <q-select outlined dense options-dense v-model="timezone" :options="allTimezones" class="col-8" />
</q-card-section> </q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-10">Check interval:</div> <div class="col-10">Run checks every:</div>
<q-input <q-input
dense dense
type="number" type="number"
@@ -90,7 +90,7 @@
/> />
</q-card-section> </q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-10">Send an overdue alert if the server has not reported in after:</div> <div class="col-10">Send an overdue alert if the agent has not reported in after:</div>
<q-input <q-input
dense dense
type="number" type="number"

View File

@@ -87,6 +87,9 @@ export default {
name: "InstallAgent", name: "InstallAgent",
mixins: [mixins], mixins: [mixins],
components: { AgentDownload }, components: { AgentDownload },
props: {
sitepk: Number,
},
data() { data() {
return { return {
client_options: [], client_options: [],
@@ -110,8 +113,19 @@ export default {
.get("/clients/clients/") .get("/clients/clients/")
.then(r => { .then(r => {
this.client_options = this.formatClientOptions(r.data); this.client_options = this.formatClientOptions(r.data);
if (this.sitepk !== undefined && this.sitepk !== null) {
this.client_options.forEach(client => {
let site = client.sites.find(site => site.id === this.sitepk);
if (site !== undefined) {
this.client = client;
this.site = { value: site.id, label: site.name };
}
});
} else {
this.client = this.client_options[0]; this.client = this.client_options[0];
this.site = this.sites[0]; this.site = this.sites[0];
}
this.$q.loading.hide(); this.$q.loading.hide();
}) })
.catch(() => { .catch(() => {

View File

@@ -93,7 +93,7 @@
<div class="col-3">Day of month to run:</div> <div class="col-3">Day of month to run:</div>
<div class="col-4"></div> <div class="col-4"></div>
<q-select <q-select
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
dense dense
class="col-5" class="col-5"
outlined outlined
@@ -107,7 +107,7 @@
<div class="col-3">Scheduled Time:</div> <div class="col-3">Scheduled Time:</div>
<div class="col-4"></div> <div class="col-4"></div>
<q-select <q-select
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
dense dense
class="col-5" class="col-5"
outlined outlined
@@ -122,43 +122,43 @@
> >
<div class="q-gutter-sm"> <div class="q-gutter-sm">
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="1" :val="1"
label="Monday" label="Monday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="2" :val="2"
label="Tuesday" label="Tuesday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="3" :val="3"
label="Wednesday" label="Wednesday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="4" :val="4"
label="Thursday" label="Thursday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="5" :val="5"
label="Friday" label="Friday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="6" :val="6"
label="Saturday" label="Saturday"
/> />
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.run_time_frequency === 'inherit'" :disable="winupdatepolicy.run_time_frequency === 'inherit'"
v-model="winupdatepolicy.run_time_days" v-model="winupdatepolicy.run_time_days"
:val="0" :val="0"
label="Sunday" label="Sunday"
@@ -186,16 +186,13 @@
<hr /> <hr />
<q-card-section class="row" v-if="!policy"> <q-card-section class="row" v-if="!policy">
<div class="col-5"> <div class="col-5">
<q-checkbox <q-checkbox v-model="winupdatepolicy.reprocess_failed_inherit" label="Inherit failed patch settings" />
v-model="winupdatepolicy.reprocess_failed_inherit"
label="Inherit failed patch settings"
/>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-5"> <div class="col-5">
<q-checkbox <q-checkbox
:disabled="winupdatepolicy.reprocess_failed_inherit" :disable="winupdatepolicy.reprocess_failed_inherit"
v-model="winupdatepolicy.reprocess_failed" v-model="winupdatepolicy.reprocess_failed"
label="Reprocess failed patches" label="Reprocess failed patches"
/> />
@@ -205,7 +202,7 @@
<q-input <q-input
dense dense
v-model.number="winupdatepolicy.reprocess_failed_times" v-model.number="winupdatepolicy.reprocess_failed_times"
:disabled="winupdatepolicy.reprocess_failed_inherit" :disable="winupdatepolicy.reprocess_failed_inherit"
type="number" type="number"
filled filled
label="Times" label="Times"
@@ -215,7 +212,7 @@
<div class="col-3"></div> <div class="col-3"></div>
<q-checkbox <q-checkbox
v-model="winupdatepolicy.email_if_fail" v-model="winupdatepolicy.email_if_fail"
:disabled="winupdatepolicy.reprocess_failed_inherit" :disable="winupdatepolicy.reprocess_failed_inherit"
label="Send an email when patch installation fails" label="Send an email when patch installation fails"
/> />
</q-card-section> </q-card-section>

View File

@@ -33,7 +33,6 @@
</template> </template>
<script> <script>
import axios from "axios";
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import { date } from "quasar"; import { date } from "quasar";
@@ -50,11 +49,12 @@ export default {
scheduleReboot() { scheduleReboot() {
this.$q.loading.show({ message: "Contacting agent..." }); this.$q.loading.show({ message: "Contacting agent..." });
const data = { pk: this.selectedAgentPk, datetime: this.datetime }; const data = { pk: this.selectedAgentPk, datetime: this.datetime };
axios this.$axios
.post("/agents/rebootlater/", data) .patch("/agents/reboot/", data)
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();
this.$emit("close"); this.$emit("close");
this.$emit("edited");
this.confirmReboot(r.data); this.confirmReboot(r.data);
}) })
.catch(e => { .catch(e => {

View File

@@ -77,6 +77,7 @@ export default {
.post("/agents/updateagents/", data) .post("/agents/updateagents/", data)
.then(r => { .then(r => {
this.$emit("close"); this.$emit("close");
this.$emit("edited");
this.notifySuccess("Agents will now be updated"); this.notifySuccess("Agents will now be updated");
}) })
.catch(() => this.notifyError("Something went wrong")); .catch(() => this.notifyError("Something went wrong"));

View File

@@ -0,0 +1,100 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Server Maintenance</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="submit">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Actions"
v-model="action"
:options="actions"
emit-value
map-options
@input="clear"
/>
</q-card-section>
<q-card-section v-if="action === 'prune_db'">
<q-checkbox v-model="prune_tables" val="agent_outages" label="Agent outage">
<q-tooltip>Removes resolved agent outage records</q-tooltip>
</q-checkbox>
<q-checkbox v-model="prune_tables" val="audit_logs" label="Audit Log">
<q-tooltip>Removes agent check results</q-tooltip>
</q-checkbox>
<q-checkbox v-model="prune_tables" val="pending_actions" label="Pending Actions">
<q-tooltip>Removes completed pending actions</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-actions align="left">
<q-btn label="Submit" color="primary" type="submit" class="full-width" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
name: "ServerMaintenance",
mixins: [mixins],
data() {
return {
action: null,
prune_tables: [],
actions: [
{
label: "Reload Nats Configuration",
value: "reload_nats",
},
{
label: "Remove Orphaned Tasks",
value: "rm_orphaned_tasks",
},
{
label: "Prune DB Tables",
value: "prune_db",
},
],
};
},
methods: {
clear() {
this.prune_tables = [];
},
submit() {
this.$q.loading.show();
let data = {
action: this.action,
prune_tables: this.prune_tables,
};
this.$axios
.post("core/servermaintenance/", data)
.then(r => {
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.data);
});
},
},
};
</script>

View File

@@ -11,7 +11,7 @@
<div class="col"> <div class="col">
<q-btn <q-btn
label="Cancel Action" label="Cancel Action"
:disable="selectedRow === null || selectedStatus === 'completed' || actionType === 'taskaction'" :disable="selectedRow === null || selectedStatus === 'completed' || actionType !== 'schedreboot'"
color="red" color="red"
icon="cancel" icon="cancel"
dense dense
@@ -98,26 +98,26 @@ export default {
hostname: "", hostname: "",
pagination: { pagination: {
rowsPerPage: 0, rowsPerPage: 0,
sortBy: "due", sortBy: "status",
descending: true, descending: false,
}, },
all_columns: [ all_columns: [
{ name: "id", field: "id" }, { name: "id", field: "id" },
{ name: "status", field: "status" }, { name: "status", field: "status" },
{ name: "type", label: "Type", align: "left", sortable: true }, { name: "type", label: "Type", field: "action_type", align: "left", sortable: true },
{ name: "due", label: "Due", field: "due", align: "left", sortable: true }, { name: "due", label: "Due", field: "due", align: "left", sortable: true },
{ name: "desc", label: "Description", align: "left", sortable: true }, { name: "desc", label: "Description", field: "description", align: "left", sortable: true },
{ name: "agent", label: "Agent", align: "left", sortable: true }, { name: "agent", label: "Agent", field: "hostname", align: "left", sortable: true },
{ name: "client", label: "Client", align: "left", sortable: true }, { name: "client", label: "Client", field: "client", align: "left", sortable: true },
{ name: "site", label: "Site", align: "left", sortable: true }, { name: "site", label: "Site", field: "site", align: "left", sortable: true },
], ],
all_visibleColumns: ["type", "due", "desc", "agent", "client", "site"], all_visibleColumns: ["type", "due", "desc", "agent", "client", "site"],
agent_columns: [ agent_columns: [
{ name: "id", field: "id" }, { name: "id", field: "id" },
{ name: "status", field: "status" }, { name: "status", field: "status" },
{ name: "type", label: "Type", align: "left", sortable: true }, { name: "type", label: "Type", field: "action_type", align: "left", sortable: true },
{ name: "due", label: "Due", field: "due", align: "left", sortable: true }, { name: "due", label: "Due", field: "due", align: "left", sortable: true },
{ name: "desc", label: "Description", align: "left", sortable: true }, { name: "desc", label: "Description", field: "description", align: "left", sortable: true },
], ],
agent_visibleColumns: ["type", "due", "desc"], agent_visibleColumns: ["type", "due", "desc"],
}; };
@@ -152,6 +152,7 @@ export default {
.then(r => { .then(r => {
this.$q.loading.hide(); this.$q.loading.hide();
this.getPendingActions(); this.getPendingActions();
this.$emit("edited");
this.notifySuccess(r.data, 3000); this.notifySuccess(r.data, 3000);
}) })
.catch(e => { .catch(e => {

View File

@@ -173,13 +173,13 @@ export default {
timeout: 120, timeout: 120,
}, },
dayOptions: [ dayOptions: [
{ label: "Monday", value: 0 }, { label: "Monday", value: "Monday" },
{ label: "Tuesday", value: 1 }, { label: "Tuesday", value: "Tuesday" },
{ label: "Wednesday", value: 2 }, { label: "Wednesday", value: "Wednesday" },
{ label: "Thursday", value: 3 }, { label: "Thursday", value: "Thursday" },
{ label: "Friday", value: 4 }, { label: "Friday", value: "Friday" },
{ label: "Saturday", value: 5 }, { label: "Saturday", value: "Saturday" },
{ label: "Sunday", value: 6 }, { label: "Sunday", value: "Sunday" },
], ],
}; };
}, },

View File

@@ -136,6 +136,18 @@
<q-item-section>{{ menuMaintenanceText(props.node) }}</q-item-section> <q-item-section>{{ menuMaintenanceText(props.node) }}</q-item-section>
</q-item> </q-item>
<q-item
v-if="props.node.children === undefined"
clickable
v-close-popup
@click="showInstallAgent(props.node)"
>
<q-item-section side>
<q-icon name="cloud_download" />
</q-item-section>
<q-item-section>Install Agent</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showPolicyAdd(props.node)"> <q-item clickable v-close-popup @click="showPolicyAdd(props.node)">
<q-item-section side> <q-item-section side>
<q-icon name="policy" /> <q-icon name="policy" />
@@ -217,6 +229,16 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item>
<q-item-section side>
<q-checkbox v-model="filterActionsPending" />
</q-item-section>
<q-item-section>
<q-item-label>Actions Pending</q-item-label>
</q-item-section>
</q-item>
<q-item> <q-item>
<q-item-section side> <q-item-section side>
<q-checkbox v-model="filterRebootNeeded" /> <q-checkbox v-model="filterRebootNeeded" />
@@ -328,6 +350,10 @@
<q-dialog v-model="showPolicyAddModal"> <q-dialog v-model="showPolicyAddModal">
<PolicyAdd @close="showPolicyAddModal = false" :type="policyAddType" :pk="parseInt(policyAddPk)" /> <PolicyAdd @close="showPolicyAddModal = false" :type="policyAddType" :pk="parseInt(policyAddPk)" />
</q-dialog> </q-dialog>
<!-- add policy modal -->
<q-dialog v-model="showInstallAgentModal" @hide="closeInstallAgent">
<InstallAgent @close="closeInstallAgent" :sitepk="parseInt(sitePk)" />
</q-dialog>
</q-layout> </q-layout>
</template> </template>
@@ -342,6 +368,7 @@ import AlertsIcon from "@/components/AlertsIcon";
import PolicyAdd from "@/components/automation/modals/PolicyAdd"; import PolicyAdd from "@/components/automation/modals/PolicyAdd";
import ClientsForm from "@/components/modals/clients/ClientsForm"; import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm"; import SitesForm from "@/components/modals/clients/SitesForm";
import InstallAgent from "@/components/modals/agents/InstallAgent";
export default { export default {
components: { components: {
@@ -352,6 +379,7 @@ export default {
PolicyAdd, PolicyAdd,
ClientsForm, ClientsForm,
SitesForm, SitesForm,
InstallAgent,
}, },
data() { data() {
return { return {
@@ -360,6 +388,8 @@ export default {
showSitesFormModal: false, showSitesFormModal: false,
showPolicyAddModal: false, showPolicyAddModal: false,
deleteEditModalPk: null, deleteEditModalPk: null,
showInstallAgentModal: false,
sitePk: null,
clientOp: null, clientOp: null,
policyAddType: null, policyAddType: null,
policyAddPk: null, policyAddPk: null,
@@ -379,6 +409,7 @@ export default {
filterTextLength: 0, filterTextLength: 0,
filterAvailability: "all", filterAvailability: "all",
filterPatchesPending: false, filterPatchesPending: false,
filterActionsPending: false,
filterChecksFailing: false, filterChecksFailing: false,
filterRebootNeeded: false, filterRebootNeeded: false,
currentTRMMVersion: null, currentTRMMVersion: null,
@@ -446,6 +477,12 @@ export default {
align: "left", align: "left",
sortable: true, sortable: true,
}, },
{
name: "pendingactions",
field: "pending_actions",
align: "left",
sortable: true,
},
{ {
name: "agentstatus", name: "agentstatus",
field: "status", field: "status",
@@ -483,6 +520,7 @@ export default {
"description", "description",
"user", "user",
"patchespending", "patchespending",
"pendingactions",
"agentstatus", "agentstatus",
"needsreboot", "needsreboot",
"lastseen", "lastseen",
@@ -601,6 +639,14 @@ export default {
this.deleteEditModalPk = null; this.deleteEditModalPk = null;
this.clientOp = null; this.clientOp = null;
}, },
showInstallAgent(node) {
this.sitePk = node.id;
this.showInstallAgentModal = true;
},
closeInstallAgent() {
this.showInstallAgentModal = false;
this.sitePk = null;
},
reload() { reload() {
this.$store.dispatch("reload"); this.$store.dispatch("reload");
}, },
@@ -655,6 +701,7 @@ export default {
this.filterPatchesPending = false; this.filterPatchesPending = false;
this.filterRebootNeeded = false; this.filterRebootNeeded = false;
this.filterChecksFailing = false; this.filterChecksFailing = false;
this.filterActionsPending = false;
this.filterAvailability = "all"; this.filterAvailability = "all";
this.search = ""; this.search = "";
}, },
@@ -675,6 +722,10 @@ export default {
filterText += "is:patchespending "; filterText += "is:patchespending ";
} }
if (this.filterActionsPending) {
filterText += "is:actionspending ";
}
if (this.filterChecksFailing) { if (this.filterChecksFailing) {
filterText += "is:checksfailing "; filterText += "is:checksfailing ";
} }
@@ -723,6 +774,7 @@ export default {
isFilteringTable() { isFilteringTable() {
return ( return (
this.filterPatchesPending || this.filterPatchesPending ||
this.filterActionsPending ||
this.filterChecksFailing || this.filterChecksFailing ||
this.filterRebootNeeded || this.filterRebootNeeded ||
this.filterAvailability !== "all" this.filterAvailability !== "all"