Skip to content

Commit

Permalink
Release 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
wh1te909 committed Mar 5, 2025
2 parents 400b1a9 + 0572009 commit 46e71cf
Show file tree
Hide file tree
Showing 37 changed files with 813 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- name: Codestyle black
working-directory: api
run: |
black --exclude migrations/ --check tacticalrmm
black --exclude migrations/ --check --diff tacticalrmm
if [ $? -ne 0 ]; then
exit 1
fi
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ coverage.xml
setup_dev.yml
11env/
query_schema.json
gunicorn_config.py
gunicorn_config.py
set_local_config.py
5 changes: 4 additions & 1 deletion api/tacticalrmm/agents/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import logging
import random
import re
from collections import Counter
from contextlib import suppress
Expand Down Expand Up @@ -584,7 +585,9 @@ def check_run_interval(self) -> int:
# don't allow check runs less than 15s
interval = 15 if check.run_interval < 15 else check.run_interval

return interval
return interval + random.randint(
*getattr(settings, "CHECK_INTERVAL_JITTER", (1, 60))
)

def run_script(
self,
Expand Down
5 changes: 3 additions & 2 deletions api/tacticalrmm/alerts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,9 @@ def handle_alert_failure(
and dashboard_severities
and alert.severity in dashboard_severities
):
alert.hidden = False
alert.save(update_fields=["hidden"])
if alert.hidden is not False:
alert.hidden = False
alert.save(update_fields=["hidden"])

# TODO rework this
if alert.severity == AlertSeverity.INFO and not core.notify_on_info_alerts:
Expand Down
8 changes: 6 additions & 2 deletions api/tacticalrmm/apiv3/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from django.utils import timezone as djangotime
from model_bakery import baker

Expand All @@ -12,7 +14,8 @@ def setUp(self):
self.setup_coresettings()
self.agent = baker.make_recipe("agents.agent")

def test_get_checks(self):
@patch("agents.models.random.randint", return_value=0)
def test_get_checks(self, mock_randint):
agent = baker.make_recipe("agents.agent")
url = f"/api/v3/{agent.agent_id}/checkrunner/"

Expand Down Expand Up @@ -67,7 +70,8 @@ def test_get_checks(self):

self.check_not_authenticated("get", url)

def test_checkrunner_interval(self):
@patch("agents.models.random.randint", return_value=0)
def test_checkrunner_interval(self, mock_randint):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
Expand Down
13 changes: 6 additions & 7 deletions api/tacticalrmm/apiv3/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
GoArch,
MeshAgentIdent,
PAStatus,
TaskRunStatus,
)
from tacticalrmm.helpers import make_random_password, notify_error
from tacticalrmm.utils import reload_nats
Expand Down Expand Up @@ -363,7 +364,9 @@ def patch(self, request, pk, agentid):
serializer = TaskResultSerializer(data=request.data, partial=True)

serializer.is_valid(raise_exception=True)
task_result = serializer.save(last_run=djangotime.now())
task_result = serializer.save(
last_run=djangotime.now(), run_status=TaskRunStatus.COMPLETED
)

AgentHistory.objects.create(
agent=agent,
Expand All @@ -385,12 +388,8 @@ def patch(self, request, pk, agentid):
CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
)

if task_result:
task_result.status = status
task_result.save(update_fields=["status"])
else:
task_result.status = status
task.save(update_fields=["status"])
task_result.status = status
task_result.save(update_fields=["status"])

if status == CheckStatus.PASSING:
if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
Expand Down
3 changes: 2 additions & 1 deletion api/tacticalrmm/automation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
if policy and policy.active and policy.pk not in processed_policies:
processed_policies.append(policy.pk)
for task in policy.autotasks.all():
tasks.append(task)
if agent.plat in task.task_supported_platforms:
tasks.append(task)

return tasks

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import autotasks.models
import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("autotasks", "0040_alter_taskresult_id"),
]

operations = [
migrations.AddField(
model_name="automatedtask",
name="task_supported_platforms",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("windows", "Windows"),
("linux", "Linux"),
("darwin", "macOS"),
],
max_length=30,
),
default=autotasks.models.default_task_supported_platforms,
size=None,
),
),
migrations.AddField(
model_name="taskresult",
name="locked_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="taskresult",
name="run_status",
field=models.CharField(
blank=True,
choices=[("running", "Running"), ("completed", "Completed")],
max_length=30,
null=True,
),
),
]
46 changes: 42 additions & 4 deletions api/tacticalrmm/autotasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
Expand All @@ -18,7 +19,9 @@
from tacticalrmm.constants import (
FIELDS_TRIGGER_TASK_UPDATE_AGENT,
POLICY_TASK_FIELDS_TO_COPY,
AgentPlat,
AlertSeverity,
TaskRunStatus,
TaskStatus,
TaskSyncStatus,
TaskType,
Expand Down Expand Up @@ -46,6 +49,10 @@ def generate_task_name() -> str:
return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35))


def default_task_supported_platforms() -> list[str]:
return [AgentPlat.WINDOWS]


logger = logging.getLogger("trmm")


Expand Down Expand Up @@ -135,6 +142,11 @@ class AutomatedTask(BaseAuditModel):
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1)

task_supported_platforms = ArrayField(
models.CharField(max_length=30, choices=AgentPlat.choices),
default=default_task_supported_platforms,
)

# deprecated
managed_by_policy = models.BooleanField(default=False)

Expand All @@ -159,15 +171,23 @@ def save(self, *args, **kwargs) -> None:
for field in self.fields_that_trigger_task_update_on_agent:
if getattr(self, field) != getattr(old_task, field):
if self.policy:
TaskResult.objects.exclude(
sync_status=TaskSyncStatus.INITIAL
).filter(task__policy_id=self.policy.id).update(
TaskResult.objects.select_related("agent", "task").defer(
"agent__services", "agent__wmi_detail"
).exclude(sync_status=TaskSyncStatus.INITIAL).filter(
agent__plat=AgentPlat.WINDOWS,
task__policy_id=self.policy.id,
).update(
sync_status=TaskSyncStatus.NOT_SYNCED
)
else:
TaskResult.objects.filter(agent=self.agent, task=self).update(
TaskResult.objects.select_related("agent", "task").defer(
"agent__services", "agent__wmi_detail"
).filter(
agent=self.agent, task=self, agent__plat=AgentPlat.WINDOWS
).update(
sync_status=TaskSyncStatus.NOT_SYNCED
)
break

def delete(self, *args, **kwargs):
# if task is a policy task clear cache on everything
Expand Down Expand Up @@ -334,6 +354,11 @@ def create_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
task_result = TaskResult(agent=agent, task=self)
task_result.save()

if agent.is_posix:
task_result.sync_status = TaskSyncStatus.SYNCED
task_result.save(update_fields=["sync_status"])
return "ok"

nats_data = {
"func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(),
Expand Down Expand Up @@ -370,6 +395,11 @@ def modify_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
task_result = TaskResult(agent=agent, task=self)
task_result.save()

if agent.is_posix:
task_result.sync_status = TaskSyncStatus.SYNCED
task_result.save(update_fields=["sync_status"])
return "ok"

nats_data = {
"func": "schedtask",
"schedtaskpayload": self.generate_nats_task_payload(),
Expand Down Expand Up @@ -406,6 +436,10 @@ def delete_task_on_agent(self, agent: "Optional[Agent]" = None) -> str:
task_result = TaskResult(agent=agent, task=self)
task_result.save()

if agent.is_posix:
self.delete()
return "ok"

nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name},
Expand Down Expand Up @@ -492,6 +526,10 @@ class Meta:
sync_status = models.CharField(
max_length=100, choices=TaskSyncStatus.choices, default=TaskSyncStatus.INITIAL
)
locked_at = models.DateTimeField(null=True, blank=True)
run_status = models.CharField(
max_length=30, choices=TaskRunStatus.choices, null=True, blank=True
)

def __str__(self):
return f"{self.agent.hostname} - {self.task}"
Expand Down
2 changes: 1 addition & 1 deletion api/tacticalrmm/autotasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def remove_orphaned_win_tasks(self) -> str:
exclude_tasks = ("TacticalRMM_SchedReboot",)

for agent in _get_agent_qs():
if agent.status == AGENT_STATUS_ONLINE:
if not agent.is_posix and agent.status == AGENT_STATUS_ONLINE:
names = [task.win_task_name for task in agent.get_tasks_with_policies()]
items.append(AgentTup._make([agent.agent_id, names]))

Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from tacticalrmm.constants import TaskType
from tacticalrmm.test import TacticalTestCase

from .models import AutomatedTask, TaskResult, TaskSyncStatus
from .serializers import TaskSerializer
from .tasks import create_win_task_schedule, run_win_task
from autotasks.models import AutomatedTask, TaskResult, TaskSyncStatus
from autotasks.serializers import TaskSerializer
from autotasks.tasks import create_win_task_schedule, run_win_task

base_url = "/tasks"

Expand Down
Loading

0 comments on commit 46e71cf

Please sign in to comment.