Compare commits

..

16 Commits

Author SHA1 Message Date
wh1te909
617738bb28 Release 0.1.3 2020-11-06 21:06:04 +00:00
wh1te909
f6ac15d790 fix auto update for older agents 2020-11-06 21:04:44 +00:00
Tragic Bronson
144a3dedbb Merge pull request #165 from sadnub/develop
fix strange test issue
2020-11-02 09:30:57 -08:00
sadnub
f90d966f1a fix strange test issue 2020-11-02 10:18:07 -05:00
wh1te909
b188e2ea97 more agent tasks tests 2020-11-02 10:20:22 +00:00
wh1te909
b63b2002a9 Release 0.1.2 2020-11-02 06:54:28 +00:00
wh1te909
059edc36e4 version 0.1.2 2020-11-02 06:53:21 +00:00
wh1te909
902034ecf0 fix agent update for 32bit agents 2020-11-02 06:46:35 +00:00
wh1te909
4d27f2b594 Release 0.1.1 2020-11-01 23:50:29 +00:00
wh1te909
f73ea8f9f4 bump version 2020-11-01 23:50:05 +00:00
wh1te909
596ec3eb2c add win task test 2020-11-01 23:43:24 +00:00
wh1te909
cb71319ff0 Merge pull request #161 from sadnub/develop
make run once task run as soon as agent is online
2020-11-01 14:26:03 -08:00
sadnub
7b7164a9a2 fix warning message about tz 2020-11-01 17:16:58 -05:00
sadnub
721ce8f91a fix time zone issues 2020-11-01 16:26:37 -05:00
sadnub
9fdbae986c handle task times in the past 2020-11-01 13:32:20 -05:00
sadnub
9535a9fa3f make run once task run as soon as agent is online 2020-11-01 10:53:14 -05:00
8 changed files with 506 additions and 23 deletions

View File

@@ -86,27 +86,35 @@ class Agent(BaseAuditModel):
@property
def arch(self):
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"
elif "32bit" in self.operating_system:
elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
return "32"
return "64"
return None
@property
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
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
def win_inno_exe(self):
return (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
if self.arch == "64"
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
)
if self.arch == "64":
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
elif self.arch == "32":
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
return None
@property
def status(self):

View File

@@ -30,6 +30,14 @@ def send_agent_update_task(pks, version):
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
@@ -42,6 +50,9 @@ def send_agent_update_task(pks, version):
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -49,6 +60,7 @@ def send_agent_update_task(pks, version):
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)
@@ -56,6 +68,7 @@ def send_agent_update_task(pks, version):
def auto_self_agent_update_task():
core = CoreSettings.objects.first()
if not core.agent_auto_update:
logger.info("Agent auto update is disabled. Skipping.")
return
q = Agent.objects.only("pk", "version")
@@ -64,12 +77,21 @@ def auto_self_agent_update_task():
for i in q
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))
for chunk in chunks:
for pk in chunk:
agent = Agent.objects.get(pk=pk)
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.salt_id}. Skipping."
)
continue
# golang agent only backwards compatible with py agent 0.11.2
# force an upgrade to the latest python agent if version < 0.11.2
if pyver.parse(agent.version) < pyver.parse("0.11.2"):
@@ -82,6 +104,9 @@ def auto_self_agent_update_task():
else:
url = agent.winagent_dl
inno = agent.win_inno_exe
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -89,6 +114,7 @@ def auto_self_agent_update_task():
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)

View File

@@ -6,11 +6,22 @@ from model_bakery import baker
from itertools import cycle
from django.conf import settings
from django.utils import timezone as djangotime
from tacticalrmm.test import BaseTestCase, TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
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
@@ -779,3 +790,224 @@ class TestAgentViewsNew(TacticalTestCase):
self.assertEqual(r.status_code, 400)
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")

View File

@@ -888,18 +888,22 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
clients = baker.make("clients.Client", client=seq("Default"), _quantity=2)
baker.make(
"clients.Site", client=cycle(clients), site=seq("Default"), _quantity=4
clients = baker.make("clients.Client", client=seq("Client"), _quantity=2)
sites = baker.make(
"clients.Site", client=cycle(clients), site=seq("Site"), _quantity=4
)
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(
"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.server_policy = policy
core.workstation_policy = policy
@@ -1027,7 +1031,7 @@ class TestPolicyTasks(TacticalTestCase):
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
clients = baker.make(

View File

@@ -1,7 +1,8 @@
from loguru import logger
from tacticalrmm.celery import app
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 logs.models import PendingAction
@@ -46,6 +47,16 @@ def create_win_task_schedule(pk, pending_action=False):
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(
timeout=20,
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")}"',
"ac_only=False",
"stop_if_on_batteries=False",
"start_when_available=True",
],
)

View File

@@ -1,11 +1,13 @@
from unittest.mock import patch, call
from model_bakery import baker
from django.utils import timezone as djangotime
from tacticalrmm.test import TacticalTestCase
from .models import AutomatedTask
from logs.models import PendingAction
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):
@@ -281,3 +283,201 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
salt_api_async.return_value = "Response 200"
ret = run_win_task.s(self.task1.pk).apply()
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")

View File

@@ -25,8 +25,9 @@ def core_maintenance_tasks():
# cleanup expired runonce tasks
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:
agent_tz = pytz.timezone(task.agent.timezone)

View File

@@ -10,7 +10,7 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.1.0"
TRMM_VERSION = "0.1.3"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser