Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617738bb28 | ||
|
|
f6ac15d790 | ||
|
|
144a3dedbb | ||
|
|
f90d966f1a | ||
|
|
b188e2ea97 | ||
|
|
b63b2002a9 | ||
|
|
059edc36e4 | ||
|
|
902034ecf0 | ||
|
|
4d27f2b594 | ||
|
|
f73ea8f9f4 | ||
|
|
596ec3eb2c | ||
|
|
cb71319ff0 | ||
|
|
7b7164a9a2 | ||
|
|
721ce8f91a | ||
|
|
9fdbae986c | ||
|
|
9535a9fa3f |
@@ -86,27 +86,35 @@ class Agent(BaseAuditModel):
|
|||||||
@property
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
if self.operating_system is not None:
|
if self.operating_system is not None:
|
||||||
if "64bit" in self.operating_system:
|
if "64 bit" in self.operating_system or "64bit" in self.operating_system:
|
||||||
return "64"
|
return "64"
|
||||||
elif "32bit" in self.operating_system:
|
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
|
||||||
return "32"
|
return "32"
|
||||||
return "64"
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def winagent_dl(self):
|
def winagent_dl(self):
|
||||||
return settings.DL_64 if self.arch == "64" else settings.DL_32
|
if self.arch == "64":
|
||||||
|
return settings.DL_64
|
||||||
|
elif self.arch == "32":
|
||||||
|
return settings.DL_32
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def winsalt_dl(self):
|
def winsalt_dl(self):
|
||||||
return settings.SALT_64 if self.arch == "64" else settings.SALT_32
|
if self.arch == "64":
|
||||||
|
return settings.SALT_64
|
||||||
|
elif self.arch == "32":
|
||||||
|
return settings.SALT_32
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def win_inno_exe(self):
|
def win_inno_exe(self):
|
||||||
return (
|
if self.arch == "64":
|
||||||
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||||
if self.arch == "64"
|
elif self.arch == "32":
|
||||||
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
||||||
)
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ def send_agent_update_task(pks, version):
|
|||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
for pk in chunk:
|
for pk in chunk:
|
||||||
agent = Agent.objects.get(pk=pk)
|
agent = Agent.objects.get(pk=pk)
|
||||||
|
|
||||||
|
# skip if we can't determine the arch
|
||||||
|
if agent.arch is None:
|
||||||
|
logger.warning(
|
||||||
|
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
# golang agent only backwards compatible with py agent 0.11.2
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
# force an upgrade to the latest python agent if version < 0.11.2
|
||||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
||||||
@@ -42,6 +50,9 @@ 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}"
|
||||||
|
)
|
||||||
r = agent.salt_api_async(
|
r = agent.salt_api_async(
|
||||||
func="win_agent.do_agent_update_v2",
|
func="win_agent.do_agent_update_v2",
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -49,6 +60,7 @@ def send_agent_update_task(pks, version):
|
|||||||
"url": url,
|
"url": url,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
logger.info(f"{agent.salt_id}: {r}")
|
||||||
sleep(10)
|
sleep(10)
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +68,7 @@ def send_agent_update_task(pks, version):
|
|||||||
def auto_self_agent_update_task():
|
def auto_self_agent_update_task():
|
||||||
core = CoreSettings.objects.first()
|
core = CoreSettings.objects.first()
|
||||||
if not core.agent_auto_update:
|
if not core.agent_auto_update:
|
||||||
|
logger.info("Agent auto update is disabled. Skipping.")
|
||||||
return
|
return
|
||||||
|
|
||||||
q = Agent.objects.only("pk", "version")
|
q = Agent.objects.only("pk", "version")
|
||||||
@@ -64,12 +77,21 @@ 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))
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
for pk in chunk:
|
for pk in chunk:
|
||||||
agent = Agent.objects.get(pk=pk)
|
agent = Agent.objects.get(pk=pk)
|
||||||
|
|
||||||
|
# skip if we can't determine the arch
|
||||||
|
if agent.arch is None:
|
||||||
|
logger.warning(
|
||||||
|
f"Unable to determine arch on {agent.salt_id}. Skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# golang agent only backwards compatible with py agent 0.11.2
|
# golang agent only backwards compatible with py agent 0.11.2
|
||||||
# force an upgrade to the latest python agent if version < 0.11.2
|
# force an upgrade to the latest python agent if version < 0.11.2
|
||||||
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
|
||||||
@@ -82,6 +104,9 @@ 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}"
|
||||||
|
)
|
||||||
r = agent.salt_api_async(
|
r = agent.salt_api_async(
|
||||||
func="win_agent.do_agent_update_v2",
|
func="win_agent.do_agent_update_v2",
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -89,6 +114,7 @@ def auto_self_agent_update_task():
|
|||||||
"url": url,
|
"url": url,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
logger.info(f"{agent.salt_id}: {r}")
|
||||||
sleep(10)
|
sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,22 @@ from model_bakery import baker
|
|||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
|
||||||
from tacticalrmm.test import BaseTestCase, TacticalTestCase
|
from tacticalrmm.test import BaseTestCase, TacticalTestCase
|
||||||
from .serializers import AgentSerializer
|
from .serializers import AgentSerializer
|
||||||
from winupdate.serializers import WinUpdatePolicySerializer
|
from winupdate.serializers import WinUpdatePolicySerializer
|
||||||
from .models import Agent
|
from .models import Agent
|
||||||
|
from .tasks import (
|
||||||
|
auto_self_agent_update_task,
|
||||||
|
update_salt_minion_task,
|
||||||
|
get_wmi_detail_task,
|
||||||
|
sync_salt_modules_task,
|
||||||
|
batch_sync_modules_task,
|
||||||
|
batch_sysinfo_task,
|
||||||
|
OLD_64_PY_AGENT,
|
||||||
|
OLD_32_PY_AGENT,
|
||||||
|
)
|
||||||
from winupdate.models import WinUpdatePolicy
|
from winupdate.models import WinUpdatePolicy
|
||||||
|
|
||||||
|
|
||||||
@@ -779,3 +790,224 @@ class TestAgentViewsNew(TacticalTestCase):
|
|||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
|
|
||||||
self.check_not_authenticated("post", url)
|
self.check_not_authenticated("post", url)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgentTasks(TacticalTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.authenticate()
|
||||||
|
self.setup_coresettings()
|
||||||
|
|
||||||
|
@patch("agents.models.Agent.salt_api_async", return_value=None)
|
||||||
|
def test_get_wmi_detail_task(self, salt_api_async):
|
||||||
|
self.agent = baker.make_recipe("agents.agent")
|
||||||
|
ret = get_wmi_detail_task.s(self.agent.pk).apply()
|
||||||
|
salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info")
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|
||||||
|
@patch("agents.models.Agent.salt_api_cmd")
|
||||||
|
def test_sync_salt_modules_task(self, salt_api_cmd):
|
||||||
|
self.agent = baker.make_recipe("agents.agent")
|
||||||
|
salt_api_cmd.return_value = {"return": [{f"{self.agent.salt_id}": []}]}
|
||||||
|
ret = sync_salt_modules_task.s(self.agent.pk).apply()
|
||||||
|
salt_api_cmd.assert_called_with(timeout=35, func="saltutil.sync_modules")
|
||||||
|
self.assertEqual(
|
||||||
|
ret.result, f"Successfully synced salt modules on {self.agent.hostname}"
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|
||||||
|
salt_api_cmd.return_value = "timeout"
|
||||||
|
ret = sync_salt_modules_task.s(self.agent.pk).apply()
|
||||||
|
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
|
||||||
|
|
||||||
|
salt_api_cmd.return_value = "error"
|
||||||
|
ret = sync_salt_modules_task.s(self.agent.pk).apply()
|
||||||
|
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
|
||||||
|
|
||||||
|
@patch("agents.models.Agent.salt_batch_async", return_value=None)
|
||||||
|
@patch("agents.tasks.sleep", return_value=None)
|
||||||
|
def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async):
|
||||||
|
# chunks of 50, 60 online should run only 2 times
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.online_agent", last_seen=djangotime.now(), _quantity=60
|
||||||
|
)
|
||||||
|
baker.make_recipe(
|
||||||
|
"agents.overdue_agent",
|
||||||
|
last_seen=djangotime.now() - djangotime.timedelta(minutes=9),
|
||||||
|
_quantity=115,
|
||||||
|
)
|
||||||
|
ret = batch_sync_modules_task.s().apply()
|
||||||
|
self.assertEqual(salt_batch_async.call_count, 2)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|
||||||
|
@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):
|
||||||
|
# chunks of 30, 70 online should run only 3 times
|
||||||
|
self.online = baker.make_recipe(
|
||||||
|
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=70
|
||||||
|
)
|
||||||
|
self.overdue = baker.make_recipe(
|
||||||
|
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=115
|
||||||
|
)
|
||||||
|
ret = batch_sysinfo_task.s().apply()
|
||||||
|
self.assertEqual(salt_batch_async.call_count, 3)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
salt_batch_async.reset_mock()
|
||||||
|
[i.delete() for i in self.online]
|
||||||
|
[i.delete() for i in self.overdue]
|
||||||
|
|
||||||
|
# test old agents, should not run
|
||||||
|
self.online_old = baker.make_recipe(
|
||||||
|
"agents.online_agent", version="0.10.2", _quantity=70
|
||||||
|
)
|
||||||
|
self.overdue_old = baker.make_recipe(
|
||||||
|
"agents.overdue_agent", version="0.10.2", _quantity=115
|
||||||
|
)
|
||||||
|
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.tasks.sleep", return_value=None)
|
||||||
|
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async):
|
||||||
|
# test 64bit golang agent
|
||||||
|
self.agent64 = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
salt_api_async.return_value = True
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_called_with(
|
||||||
|
func="win_agent.do_agent_update_v2",
|
||||||
|
kwargs={
|
||||||
|
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||||
|
"url": settings.DL_64,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
self.agent64.delete()
|
||||||
|
salt_api_async.reset_mock()
|
||||||
|
|
||||||
|
# test 32bit golang agent
|
||||||
|
self.agent32 = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
salt_api_async.return_value = True
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_called_with(
|
||||||
|
func="win_agent.do_agent_update_v2",
|
||||||
|
kwargs={
|
||||||
|
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
|
||||||
|
"url": settings.DL_32,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
self.agent32.delete()
|
||||||
|
salt_api_async.reset_mock()
|
||||||
|
|
||||||
|
# test agent that has a null os field
|
||||||
|
self.agentNone = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system=None,
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_not_called()
|
||||||
|
self.agentNone.delete()
|
||||||
|
salt_api_async.reset_mock()
|
||||||
|
|
||||||
|
# test auto update disabled in global settings
|
||||||
|
self.agent64 = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
self.coresettings.agent_auto_update = False
|
||||||
|
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_not_called()
|
||||||
|
|
||||||
|
# reset core settings
|
||||||
|
self.agent64.delete()
|
||||||
|
salt_api_async.reset_mock()
|
||||||
|
self.coresettings.agent_auto_update = True
|
||||||
|
self.coresettings.save(update_fields=["agent_auto_update"])
|
||||||
|
|
||||||
|
# test 64bit python agent
|
||||||
|
self.agent64py = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||||
|
version="0.11.1",
|
||||||
|
)
|
||||||
|
salt_api_async.return_value = True
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_called_with(
|
||||||
|
func="win_agent.do_agent_update_v2",
|
||||||
|
kwargs={
|
||||||
|
"inno": "winagent-v0.11.2.exe",
|
||||||
|
"url": OLD_64_PY_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
self.agent64py.delete()
|
||||||
|
salt_api_async.reset_mock()
|
||||||
|
|
||||||
|
# test 32bit python agent
|
||||||
|
self.agent32py = baker.make_recipe(
|
||||||
|
"agents.agent",
|
||||||
|
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
|
||||||
|
version="0.11.1",
|
||||||
|
)
|
||||||
|
salt_api_async.return_value = True
|
||||||
|
ret = auto_self_agent_update_task.s().apply()
|
||||||
|
salt_api_async.assert_called_with(
|
||||||
|
func="win_agent.do_agent_update_v2",
|
||||||
|
kwargs={
|
||||||
|
"inno": "winagent-v0.11.2-x86.exe",
|
||||||
|
"url": OLD_32_PY_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
@@ -888,18 +888,22 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
# setup data
|
# setup data
|
||||||
policy = baker.make("automation.Policy", active=True)
|
policy = baker.make("automation.Policy", active=True)
|
||||||
self.create_checks(policy=policy)
|
self.create_checks(policy=policy)
|
||||||
clients = baker.make("clients.Client", client=seq("Default"), _quantity=2)
|
clients = baker.make("clients.Client", client=seq("Client"), _quantity=2)
|
||||||
baker.make(
|
sites = baker.make(
|
||||||
"clients.Site", client=cycle(clients), site=seq("Default"), _quantity=4
|
"clients.Site", client=cycle(clients), site=seq("Site"), _quantity=4
|
||||||
)
|
)
|
||||||
server_agent = baker.make_recipe(
|
server_agent = baker.make_recipe(
|
||||||
"agents.server_agent", client="Default1", site="Default1"
|
"agents.server_agent", client=clients[0].client, site=sites[0].site
|
||||||
)
|
)
|
||||||
workstation_agent = baker.make_recipe(
|
workstation_agent = baker.make_recipe(
|
||||||
"agents.workstation_agent", client="Default1", site="Default3"
|
"agents.workstation_agent", client=clients[0].client, site=sites[2].site
|
||||||
|
)
|
||||||
|
agent1 = baker.make_recipe(
|
||||||
|
"agents.server_agent", client=clients[1].client, site=sites[1].site
|
||||||
|
)
|
||||||
|
agent2 = baker.make_recipe(
|
||||||
|
"agents.workstation_agent", client=clients[1].client, site=sites[3].site
|
||||||
)
|
)
|
||||||
agent1 = baker.make_recipe("agents.agent", client="Default2", site="Default2")
|
|
||||||
agent2 = baker.make_recipe("agents.agent", client="Default2", site="Default4")
|
|
||||||
core = CoreSettings.objects.first()
|
core = CoreSettings.objects.first()
|
||||||
core.server_policy = policy
|
core.server_policy = policy
|
||||||
core.workstation_policy = policy
|
core.workstation_policy = policy
|
||||||
@@ -1027,7 +1031,7 @@ class TestPolicyTasks(TacticalTestCase):
|
|||||||
|
|
||||||
# setup data
|
# setup data
|
||||||
policy = baker.make("automation.Policy", active=True)
|
policy = baker.make("automation.Policy", active=True)
|
||||||
tasks = baker.make(
|
baker.make(
|
||||||
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
|
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
|
||||||
)
|
)
|
||||||
clients = baker.make(
|
clients = baker.make(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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
|
||||||
from datetime import datetime as dt
|
import pytz
|
||||||
|
from django.utils import timezone as djangotime
|
||||||
|
|
||||||
from .models import AutomatedTask
|
from .models import AutomatedTask
|
||||||
from logs.models import PendingAction
|
from logs.models import PendingAction
|
||||||
@@ -46,6 +47,16 @@ def create_win_task_schedule(pk, pending_action=False):
|
|||||||
|
|
||||||
elif task.task_type == "runonce":
|
elif task.task_type == "runonce":
|
||||||
|
|
||||||
|
# check if scheduled time is in the past
|
||||||
|
agent_tz = pytz.timezone(task.agent.timezone)
|
||||||
|
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
|
||||||
|
now = djangotime.now()
|
||||||
|
if task_time_utc < now:
|
||||||
|
task.run_time_date = now.astimezone(agent_tz).replace(
|
||||||
|
tzinfo=pytz.utc
|
||||||
|
) + djangotime.timedelta(minutes=5)
|
||||||
|
task.save()
|
||||||
|
|
||||||
r = task.agent.salt_api_cmd(
|
r = task.agent.salt_api_cmd(
|
||||||
timeout=20,
|
timeout=20,
|
||||||
func="task.create_task",
|
func="task.create_task",
|
||||||
@@ -61,6 +72,7 @@ def create_win_task_schedule(pk, pending_action=False):
|
|||||||
f'start_time="{task.run_time_date.strftime("%H:%M")}"',
|
f'start_time="{task.run_time_date.strftime("%H:%M")}"',
|
||||||
"ac_only=False",
|
"ac_only=False",
|
||||||
"stop_if_on_batteries=False",
|
"stop_if_on_batteries=False",
|
||||||
|
"start_when_available=True",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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 tacticalrmm.test import TacticalTestCase
|
from tacticalrmm.test import TacticalTestCase
|
||||||
|
|
||||||
from .models import AutomatedTask
|
from .models import AutomatedTask
|
||||||
|
from logs.models import PendingAction
|
||||||
from .serializers import AutoTaskSerializer
|
from .serializers import AutoTaskSerializer
|
||||||
from .tasks import remove_orphaned_win_tasks, run_win_task
|
from .tasks import remove_orphaned_win_tasks, run_win_task, create_win_task_schedule
|
||||||
|
|
||||||
|
|
||||||
class TestAutotaskViews(TacticalTestCase):
|
class TestAutotaskViews(TacticalTestCase):
|
||||||
@@ -281,3 +283,201 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
|||||||
salt_api_async.return_value = "Response 200"
|
salt_api_async.return_value = "Response 200"
|
||||||
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")
|
||||||
|
def test_create_win_task_schedule(self, salt_api_cmd):
|
||||||
|
self.agent = baker.make_recipe("agents.agent")
|
||||||
|
|
||||||
|
task_name = AutomatedTask.generate_task_name()
|
||||||
|
# test scheduled task
|
||||||
|
self.task1 = AutomatedTask.objects.create(
|
||||||
|
agent=self.agent,
|
||||||
|
name="test task 1",
|
||||||
|
win_task_name=task_name,
|
||||||
|
task_type="scheduled",
|
||||||
|
run_time_days=[0, 1, 6],
|
||||||
|
run_time_minute="21:55",
|
||||||
|
)
|
||||||
|
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||||
|
self.assertEqual(salt_api_cmd.call_count, 1)
|
||||||
|
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.task1.pk}"',
|
||||||
|
"start_in=C:\\Program Files\\TacticalAgent",
|
||||||
|
"trigger_type=Weekly",
|
||||||
|
'start_time="21:55"',
|
||||||
|
"ac_only=False",
|
||||||
|
"stop_if_on_batteries=False",
|
||||||
|
],
|
||||||
|
kwargs={"days_of_week": ["Monday", "Tuesday", "Sunday"]},
|
||||||
|
)
|
||||||
|
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||||
|
self.assertEqual(self.task1.sync_status, "synced")
|
||||||
|
|
||||||
|
salt_api_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()
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||||
|
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||||
|
|
||||||
|
# test pending action
|
||||||
|
self.pending_action = PendingAction.objects.create(
|
||||||
|
agent=self.agent, action_type="taskaction"
|
||||||
|
)
|
||||||
|
self.assertEqual(self.pending_action.status, "pending")
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(
|
||||||
|
pk=self.task1.pk, pending_action=self.pending_action.pk
|
||||||
|
).apply()
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
self.pending_action = PendingAction.objects.get(pk=self.pending_action.pk)
|
||||||
|
self.assertEqual(self.pending_action.status, "completed")
|
||||||
|
|
||||||
|
# test runonce with future date
|
||||||
|
salt_api_cmd.reset_mock()
|
||||||
|
task_name = AutomatedTask.generate_task_name()
|
||||||
|
run_time_date = djangotime.now() + djangotime.timedelta(hours=22)
|
||||||
|
self.task2 = AutomatedTask.objects.create(
|
||||||
|
agent=self.agent,
|
||||||
|
name="test task 2",
|
||||||
|
win_task_name=task_name,
|
||||||
|
task_type="runonce",
|
||||||
|
run_time_date=run_time_date,
|
||||||
|
)
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
|
||||||
|
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.task2.pk}"',
|
||||||
|
"start_in=C:\\Program Files\\TacticalAgent",
|
||||||
|
"trigger_type=Once",
|
||||||
|
f'start_date="{run_time_date.strftime("%Y-%m-%d")}"',
|
||||||
|
f'start_time="{run_time_date.strftime("%H:%M")}"',
|
||||||
|
"ac_only=False",
|
||||||
|
"stop_if_on_batteries=False",
|
||||||
|
"start_when_available=True",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|
||||||
|
# test runonce with date in the past
|
||||||
|
salt_api_cmd.reset_mock()
|
||||||
|
task_name = AutomatedTask.generate_task_name()
|
||||||
|
run_time_date = djangotime.now() - djangotime.timedelta(days=13)
|
||||||
|
self.task3 = AutomatedTask.objects.create(
|
||||||
|
agent=self.agent,
|
||||||
|
name="test task 3",
|
||||||
|
win_task_name=task_name,
|
||||||
|
task_type="runonce",
|
||||||
|
run_time_date=run_time_date,
|
||||||
|
)
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
|
||||||
|
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")
|
||||||
|
|
||||||
|
# test checkfailure
|
||||||
|
salt_api_cmd.reset_mock()
|
||||||
|
self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent)
|
||||||
|
task_name = AutomatedTask.generate_task_name()
|
||||||
|
self.task4 = AutomatedTask.objects.create(
|
||||||
|
agent=self.agent,
|
||||||
|
name="test task 4",
|
||||||
|
win_task_name=task_name,
|
||||||
|
task_type="checkfailure",
|
||||||
|
assigned_check=self.check,
|
||||||
|
)
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
|
||||||
|
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.task4.pk}"',
|
||||||
|
"start_in=C:\\Program Files\\TacticalAgent",
|
||||||
|
"trigger_type=Once",
|
||||||
|
'start_date="1975-01-01"',
|
||||||
|
'start_time="01:00"',
|
||||||
|
"ac_only=False",
|
||||||
|
"stop_if_on_batteries=False",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|
||||||
|
# test manual
|
||||||
|
salt_api_cmd.reset_mock()
|
||||||
|
task_name = AutomatedTask.generate_task_name()
|
||||||
|
self.task5 = AutomatedTask.objects.create(
|
||||||
|
agent=self.agent,
|
||||||
|
name="test task 5",
|
||||||
|
win_task_name=task_name,
|
||||||
|
task_type="manual",
|
||||||
|
)
|
||||||
|
salt_api_cmd.return_value = True
|
||||||
|
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
|
||||||
|
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.task5.pk}"',
|
||||||
|
"start_in=C:\\Program Files\\TacticalAgent",
|
||||||
|
"trigger_type=Once",
|
||||||
|
'start_date="1975-01-01"',
|
||||||
|
'start_time="01:00"',
|
||||||
|
"ac_only=False",
|
||||||
|
"stop_if_on_batteries=False",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(ret.status, "SUCCESS")
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ def core_maintenance_tasks():
|
|||||||
|
|
||||||
# cleanup expired runonce tasks
|
# cleanup expired runonce tasks
|
||||||
tasks = AutomatedTask.objects.filter(
|
tasks = AutomatedTask.objects.filter(
|
||||||
task_type="runonce", remove_if_not_scheduled=True
|
task_type="runonce",
|
||||||
)
|
remove_if_not_scheduled=True,
|
||||||
|
).exclude(last_run=None)
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
agent_tz = pytz.timezone(task.agent.timezone)
|
agent_tz = pytz.timezone(task.agent.timezone)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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.1.0"
|
TRMM_VERSION = "0.1.3"
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user