Compare commits
	
		
			21 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d82f0cd757 | ||
| 
						 | 
					5f529e2af4 | ||
| 
						 | 
					beadd9e02b | ||
| 
						 | 
					72543789cb | ||
| 
						 | 
					5789439fa9 | ||
| 
						 | 
					f549126bcf | ||
| 
						 | 
					7197548bad | ||
| 
						 | 
					241fde783c | ||
| 
						 | 
					2b872cd1f4 | ||
| 
						 | 
					a606fb4d1d | ||
| 
						 | 
					9f9c6be38e | ||
| 
						 | 
					01ee524049 | ||
| 
						 | 
					af9cb65338 | ||
| 
						 | 
					8aa11c580b | ||
| 
						 | 
					ada627f444 | ||
| 
						 | 
					a7b6d338c3 | ||
| 
						 | 
					9f00538b97 | ||
| 
						 | 
					a085015282 | ||
| 
						 | 
					0b9c220fbb | ||
| 
						 | 
					0e3d04873d | ||
| 
						 | 
					b7578d939f | 
@@ -278,15 +278,11 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data = {"agent_dblclick_action": "editagent"}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data = {"agent_dblclick_action": "remotebg"}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data = {"agent_dblclick_action": "takecontrol"}
 | 
			
		||||
        data = {
 | 
			
		||||
            "userui": True,
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,52 +16,20 @@ from logs.models import PendingAction
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_agent_service(pk: int) -> None:
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
 | 
			
		||||
    # if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
 | 
			
		||||
    # then tacticalagent service is hung. forcefully restart it
 | 
			
		||||
    if r == "pong":
 | 
			
		||||
        logger.info(
 | 
			
		||||
            f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
 | 
			
		||||
        )
 | 
			
		||||
        data = {"func": "recover", "payload": {"mode": "tacagent"}}
 | 
			
		||||
        asyncio.run(agent.nats_cmd(data, wait=False))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_in_full(pk: int) -> None:
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
    asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def check_in_task() -> None:
 | 
			
		||||
    q = Agent.objects.only("pk", "version")
 | 
			
		||||
    agents: List[int] = [
 | 
			
		||||
        i.pk for i in q if pyver.parse(i.version) == pyver.parse("1.1.12")
 | 
			
		||||
    ]
 | 
			
		||||
    chunks = (agents[i : i + 50] for i in range(0, len(agents), 50))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            _check_in_full(pk)
 | 
			
		||||
            sleep(0.1)
 | 
			
		||||
        rand = random.randint(3, 7)
 | 
			
		||||
        sleep(rand)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def monitor_agents_task() -> None:
 | 
			
		||||
    q = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
 | 
			
		||||
    agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        _check_agent_service(agent)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_update(pk: int) -> str:
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
 | 
			
		||||
        )
 | 
			
		||||
        return "not supported"
 | 
			
		||||
 | 
			
		||||
    # skip if we can't determine the arch
 | 
			
		||||
    if agent.arch is None:
 | 
			
		||||
        logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"Unable to determine arch on {agent.hostname}. Skipping agent update."
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
 | 
			
		||||
    # removed sqlite in 1.4.0 to get rid of cgo dependency
 | 
			
		||||
@@ -77,55 +45,38 @@ def agent_update(pk: int) -> str:
 | 
			
		||||
        )
 | 
			
		||||
        url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
 | 
			
		||||
 | 
			
		||||
    if agent.has_nats:
 | 
			
		||||
        if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
 | 
			
		||||
            if agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists():
 | 
			
		||||
                action = agent.pendingactions.filter(
 | 
			
		||||
                    action_type="agentupdate", status="pending"
 | 
			
		||||
                ).last()
 | 
			
		||||
                if pyver.parse(action.details["version"]) < pyver.parse(version):
 | 
			
		||||
                    action.delete()
 | 
			
		||||
                else:
 | 
			
		||||
                    return "pending"
 | 
			
		||||
    if agent.pendingactions.filter(
 | 
			
		||||
        action_type="agentupdate", status="pending"
 | 
			
		||||
    ).exists():
 | 
			
		||||
        agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
        ).delete()
 | 
			
		||||
 | 
			
		||||
            PendingAction.objects.create(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                action_type="agentupdate",
 | 
			
		||||
                details={
 | 
			
		||||
                    "url": url,
 | 
			
		||||
                    "version": version,
 | 
			
		||||
                    "inno": inno,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            nats_data = {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": url,
 | 
			
		||||
                    "version": version,
 | 
			
		||||
                    "inno": inno,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
            asyncio.run(agent.nats_cmd(nats_data, wait=False))
 | 
			
		||||
    PendingAction.objects.create(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        action_type="agentupdate",
 | 
			
		||||
        details={
 | 
			
		||||
            "url": url,
 | 
			
		||||
            "version": version,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
        return "created"
 | 
			
		||||
    else:
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to update."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return "not supported"
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "agentupdate",
 | 
			
		||||
        "payload": {
 | 
			
		||||
            "url": url,
 | 
			
		||||
            "version": version,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    asyncio.run(agent.nats_cmd(nats_data, wait=False))
 | 
			
		||||
    return "created"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(pks: List[int], version: str) -> None:
 | 
			
		||||
    q = Agent.objects.filter(pk__in=pks)
 | 
			
		||||
    agents: List[int] = [
 | 
			
		||||
        i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
 | 
			
		||||
    ]
 | 
			
		||||
    chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
 | 
			
		||||
def send_agent_update_task(pks: List[int]) -> None:
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk)
 | 
			
		||||
@@ -154,43 +105,6 @@ def auto_self_agent_update_task() -> None:
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def get_wmi_task():
 | 
			
		||||
    agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
 | 
			
		||||
    online = [
 | 
			
		||||
        i
 | 
			
		||||
        for i in agents
 | 
			
		||||
        if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
 | 
			
		||||
    ]
 | 
			
		||||
    chunks = (online[i : i + 50] for i in range(0, len(online), 50))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent in chunk:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "wmi"}, wait=False))
 | 
			
		||||
            sleep(0.1)
 | 
			
		||||
        rand = random.randint(3, 7)
 | 
			
		||||
        sleep(rand)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def sync_sysinfo_task():
 | 
			
		||||
    agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time")
 | 
			
		||||
    online = [
 | 
			
		||||
        i
 | 
			
		||||
        for i in agents
 | 
			
		||||
        if pyver.parse(i.version) >= pyver.parse("1.1.3")
 | 
			
		||||
        and pyver.parse(i.version) <= pyver.parse("1.1.12")
 | 
			
		||||
        and i.status == "online"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    chunks = (online[i : i + 50] for i in range(0, len(online), 50))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent in chunk:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
 | 
			
		||||
            sleep(0.1)
 | 
			
		||||
        rand = random.randint(3, 7)
 | 
			
		||||
        sleep(rand)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_outage_email_task(pk):
 | 
			
		||||
    sleep(random.randint(1, 15))
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,19 @@ from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from typing import List
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase, override_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from .serializers import AgentSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from .models import Agent
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -64,12 +67,34 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    @patch("agents.tasks.send_agent_update_task.delay")
 | 
			
		||||
    def test_update_agents(self, mock_task):
 | 
			
		||||
        url = "/agents/updateagents/"
 | 
			
		||||
        data = {"pks": [1, 2, 3, 5, 10], "version": "0.11.1"}
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        pks: List[int] = list(
 | 
			
		||||
            Agent.objects.only("pk", "version").values_list("pk", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pks": pks}
 | 
			
		||||
        expected: List[int] = [
 | 
			
		||||
            i.pk
 | 
			
		||||
            for i in Agent.objects.only("pk", "version")
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called_with(pks=data["pks"], version=data["version"])
 | 
			
		||||
        mock_task.assert_called_with(pks=expected)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -162,18 +187,44 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_get_event_log(self, mock_ret):
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/geteventlog/Application/30/"
 | 
			
		||||
    def test_get_event_log(self, nats_cmd):
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/geteventlog/Application/22/"
 | 
			
		||||
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            mock_ret.return_value = json.load(f)
 | 
			
		||||
            nats_cmd.return_value = json.load(f)
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 30,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Application",
 | 
			
		||||
                    "days": str(22),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=32,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mock_ret.return_value = "timeout"
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/geteventlog/Security/6/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 180,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Security",
 | 
			
		||||
                    "days": str(6),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=182,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
@@ -736,26 +787,28 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        agent_noarch = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Error getting OS",
 | 
			
		||||
            version="1.1.11",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_noarch.pk)
 | 
			
		||||
        self.assertEqual(r, "noarch")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            PendingAction.objects.filter(
 | 
			
		||||
                agent=agent_noarch, action_type="agentupdate"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            0,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent64_111 = baker.make_recipe(
 | 
			
		||||
        agent_1111 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.11",
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_1111.pk)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_111.pk)
 | 
			
		||||
        agent64_1112 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.12",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_111.pk)
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
@@ -764,6 +817,17 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
 | 
			
		||||
        self.assertEqual(action.details["version"], "1.3.0")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
                    "version": "1.3.0",
 | 
			
		||||
                    "inno": "winagent-v1.3.0.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent_64_130 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
@@ -784,128 +848,34 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        agent64_old = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.2.1",
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = agent_update(agent64_old.pk)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
                    "version": "1.3.0",
 | 
			
		||||
                    "inno": "winagent-v1.3.0.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    """ @patch("agents.models.Agent.salt_api_async")
 | 
			
		||||
    @patch("agents.tasks.agent_update")
 | 
			
		||||
    @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(
 | 
			
		||||
    def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.0.0",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            _quantity=23,
 | 
			
		||||
        )
 | 
			
		||||
        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(
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.0.0",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
            _quantity=33,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
        r = auto_self_agent_update_task.s().apply()
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 0)
 | 
			
		||||
 | 
			
		||||
        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") """
 | 
			
		||||
        r = auto_self_agent_update_task.s().apply()
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 33)
 | 
			
		||||
 
 | 
			
		||||
@@ -59,9 +59,13 @@ def get_agent_versions(request):
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def update_agents(request):
 | 
			
		||||
    pks = request.data["pks"]
 | 
			
		||||
    version = request.data["version"]
 | 
			
		||||
    send_agent_update_task.delay(pks=pks, version=version)
 | 
			
		||||
    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
			
		||||
    pks: List[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
    send_agent_update_task.delay(pks=pks)
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -184,15 +188,16 @@ def get_event_log(request, pk, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
        "timeout": 30,
 | 
			
		||||
        "timeout": timeout,
 | 
			
		||||
        "payload": {
 | 
			
		||||
            "logname": logtype,
 | 
			
		||||
            "days": str(days),
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=32))
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,21 +26,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_mesh_info(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.pk}/meshinfo/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_winupdater(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.agent_id}/winupdater/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_sysinfo(self):
 | 
			
		||||
        # TODO replace this with golang wmi sample data
 | 
			
		||||
 | 
			
		||||
@@ -59,23 +44,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_hello_patch(self):
 | 
			
		||||
        url = "/api/v3/hello/"
 | 
			
		||||
        payload = {
 | 
			
		||||
            "agent_id": self.agent.agent_id,
 | 
			
		||||
            "logged_in_username": "None",
 | 
			
		||||
            "disks": [],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        payload["logged_in_username"] = "Bob"
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_checkrunner_interval(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
 
 | 
			
		||||
@@ -2,18 +2,13 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("checkin/", views.CheckIn.as_view()),
 | 
			
		||||
    path("hello/", views.Hello.as_view()),
 | 
			
		||||
    path("checkrunner/", views.CheckRunner.as_view()),
 | 
			
		||||
    path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
 | 
			
		||||
    path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
			
		||||
    path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
 | 
			
		||||
    path("meshexe/", views.MeshExe.as_view()),
 | 
			
		||||
    path("sysinfo/", views.SysInfo.as_view()),
 | 
			
		||||
    path("newagent/", views.NewAgent.as_view()),
 | 
			
		||||
    path("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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -36,187 +36,6 @@ from tacticalrmm.utils import notify_error, reload_nats, filter_software, Softwa
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckIn(APIView):
 | 
			
		||||
    """
 | 
			
		||||
    The agent's checkin endpoint
 | 
			
		||||
    patch: called every 45 to 110 seconds, handles agent updates and recovery
 | 
			
		||||
    put: called every 5 to 10 minutes, handles basic system info
 | 
			
		||||
    post: called once on windows service startup
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent.version = request.data["version"]
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["version", "last_seen"])
 | 
			
		||||
 | 
			
		||||
        if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
 | 
			
		||||
            last_outage = agent.agentoutages.last()
 | 
			
		||||
            last_outage.recovery_time = djangotime.now()
 | 
			
		||||
            last_outage.save(update_fields=["recovery_time"])
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert:
 | 
			
		||||
                agent_recovery_email_task.delay(pk=last_outage.pk)
 | 
			
		||||
            if agent.overdue_text_alert:
 | 
			
		||||
                agent_recovery_sms_task.delay(pk=last_outage.pk)
 | 
			
		||||
 | 
			
		||||
        recovery = agent.recoveryactions.filter(last_run=None).last()
 | 
			
		||||
        if recovery is not None:
 | 
			
		||||
            recovery.last_run = djangotime.now()
 | 
			
		||||
            recovery.save(update_fields=["last_run"])
 | 
			
		||||
            return Response(recovery.send())
 | 
			
		||||
 | 
			
		||||
        # handle agent update
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
        ).exists():
 | 
			
		||||
            update = agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).last()
 | 
			
		||||
            update.status = "completed"
 | 
			
		||||
            update.save(update_fields=["status"])
 | 
			
		||||
            return Response(update.details)
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():
 | 
			
		||||
            agent.handle_pending_actions()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        if "disks" in request.data.keys():
 | 
			
		||||
            disks = request.data["disks"]
 | 
			
		||||
            new = []
 | 
			
		||||
            # python agent
 | 
			
		||||
            if isinstance(disks, dict):
 | 
			
		||||
                for k, v in disks.items():
 | 
			
		||||
                    new.append(v)
 | 
			
		||||
            else:
 | 
			
		||||
                # golang agent
 | 
			
		||||
                for disk in disks:
 | 
			
		||||
                    tmp = {}
 | 
			
		||||
                    for k, v in disk.items():
 | 
			
		||||
                        tmp["device"] = disk["device"]
 | 
			
		||||
                        tmp["fstype"] = disk["fstype"]
 | 
			
		||||
                        tmp["total"] = bytes2human(disk["total"])
 | 
			
		||||
                        tmp["used"] = bytes2human(disk["used"])
 | 
			
		||||
                        tmp["free"] = bytes2human(disk["free"])
 | 
			
		||||
                        tmp["percent"] = int(disk["percent"])
 | 
			
		||||
                    new.append(tmp)
 | 
			
		||||
 | 
			
		||||
            serializer.save(disks=new)
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
        if "logged_in_username" in request.data.keys():
 | 
			
		||||
            if request.data["logged_in_username"] != "None":
 | 
			
		||||
                serializer.save(last_logged_in_user=request.data["logged_in_username"])
 | 
			
		||||
                return Response("ok")
 | 
			
		||||
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(last_seen=djangotime.now())
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Hello(APIView):
 | 
			
		||||
    #### DEPRECATED, for agents <= 1.1.9 ####
 | 
			
		||||
    """
 | 
			
		||||
    The agent's checkin endpoint
 | 
			
		||||
    patch: called every 30 to 120 seconds
 | 
			
		||||
    post: called on agent windows service startup
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        disks = request.data["disks"]
 | 
			
		||||
        new = []
 | 
			
		||||
        # python agent
 | 
			
		||||
        if isinstance(disks, dict):
 | 
			
		||||
            for k, v in disks.items():
 | 
			
		||||
                new.append(v)
 | 
			
		||||
        else:
 | 
			
		||||
            # golang agent
 | 
			
		||||
            for disk in disks:
 | 
			
		||||
                tmp = {}
 | 
			
		||||
                for k, v in disk.items():
 | 
			
		||||
                    tmp["device"] = disk["device"]
 | 
			
		||||
                    tmp["fstype"] = disk["fstype"]
 | 
			
		||||
                    tmp["total"] = bytes2human(disk["total"])
 | 
			
		||||
                    tmp["used"] = bytes2human(disk["used"])
 | 
			
		||||
                    tmp["free"] = bytes2human(disk["free"])
 | 
			
		||||
                    tmp["percent"] = int(disk["percent"])
 | 
			
		||||
                new.append(tmp)
 | 
			
		||||
 | 
			
		||||
        if request.data["logged_in_username"] == "None":
 | 
			
		||||
            serializer.save(last_seen=djangotime.now(), disks=new)
 | 
			
		||||
        else:
 | 
			
		||||
            serializer.save(
 | 
			
		||||
                last_seen=djangotime.now(),
 | 
			
		||||
                disks=new,
 | 
			
		||||
                last_logged_in_user=request.data["logged_in_username"],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
 | 
			
		||||
            last_outage = agent.agentoutages.last()
 | 
			
		||||
            last_outage.recovery_time = djangotime.now()
 | 
			
		||||
            last_outage.save(update_fields=["recovery_time"])
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert:
 | 
			
		||||
                agent_recovery_email_task.delay(pk=last_outage.pk)
 | 
			
		||||
            if agent.overdue_text_alert:
 | 
			
		||||
                agent_recovery_sms_task.delay(pk=last_outage.pk)
 | 
			
		||||
 | 
			
		||||
        recovery = agent.recoveryactions.filter(last_run=None).last()
 | 
			
		||||
        if recovery is not None:
 | 
			
		||||
            recovery.last_run = djangotime.now()
 | 
			
		||||
            recovery.save(update_fields=["last_run"])
 | 
			
		||||
            return Response(recovery.send())
 | 
			
		||||
 | 
			
		||||
        # handle agent update
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
        ).exists():
 | 
			
		||||
            update = agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).last()
 | 
			
		||||
            update.status = "completed"
 | 
			
		||||
            update.save(update_fields=["status"])
 | 
			
		||||
            return Response(update.details)
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():
 | 
			
		||||
            agent.handle_pending_actions()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(last_seen=djangotime.now())
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunner(APIView):
 | 
			
		||||
    """
 | 
			
		||||
    For the windows golang agent
 | 
			
		||||
@@ -292,76 +111,6 @@ class TaskRunner(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WinUpdater(APIView):
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        patches = agent.winupdates.filter(action="approve").exclude(installed=True)
 | 
			
		||||
        return Response(ApprovedUpdateSerializer(patches, many=True).data)
 | 
			
		||||
 | 
			
		||||
    # agent sends patch results as it's installing them
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        kb = request.data["kb"]
 | 
			
		||||
        results = request.data["results"]
 | 
			
		||||
        update = agent.winupdates.get(kb=kb)
 | 
			
		||||
 | 
			
		||||
        if results == "error" or results == "failed":
 | 
			
		||||
            update.result = results
 | 
			
		||||
            update.save(update_fields=["result"])
 | 
			
		||||
        elif results == "success":
 | 
			
		||||
            update.result = "success"
 | 
			
		||||
            update.downloaded = True
 | 
			
		||||
            update.installed = True
 | 
			
		||||
            update.date_installed = djangotime.now()
 | 
			
		||||
            update.save(
 | 
			
		||||
                update_fields=[
 | 
			
		||||
                    "result",
 | 
			
		||||
                    "downloaded",
 | 
			
		||||
                    "installed",
 | 
			
		||||
                    "date_installed",
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        elif results == "alreadyinstalled":
 | 
			
		||||
            update.result = "success"
 | 
			
		||||
            update.downloaded = True
 | 
			
		||||
            update.installed = True
 | 
			
		||||
            update.save(update_fields=["result", "downloaded", "installed"])
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # agent calls this after it's finished installing all patches
 | 
			
		||||
    def post(self, 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:
 | 
			
		||||
            if agent.has_nats:
 | 
			
		||||
                asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
			
		||||
                logger.info(
 | 
			
		||||
                    f"{agent.hostname} is rebooting after updates were installed."
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SysInfo(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
@@ -377,29 +126,6 @@ class SysInfo(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshInfo(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        return Response(agent.mesh_node_id)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
 | 
			
		||||
        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"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshExe(APIView):
 | 
			
		||||
    """ Sends the mesh exe to the installer """
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								api/tacticalrmm/natsapi/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/tacticalrmm/natsapi/tests.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestNatsAPIViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_nats_wmi(self):
 | 
			
		||||
        url = "/natsapi/wmi/"
 | 
			
		||||
        baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.json()["agent_ids"]), 17)
 | 
			
		||||
 | 
			
		||||
    def test_natscheckin_patch(self):
 | 
			
		||||
        from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
        url = "/natsapi/checkin/"
 | 
			
		||||
        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent_updated,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": agent_updated.winagent_dl,
 | 
			
		||||
                "version": agent_updated.version,
 | 
			
		||||
                "inno": agent_updated.win_inno_exe,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent failed to update and still on same version
 | 
			
		||||
        payload = {
 | 
			
		||||
            "func": "hello",
 | 
			
		||||
            "agent_id": agent_updated.agent_id,
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent successful update
 | 
			
		||||
        payload["version"] = settings.LATEST_AGENT_VER
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "completed")
 | 
			
		||||
        action.delete()
 | 
			
		||||
@@ -7,4 +7,7 @@ urlpatterns = [
 | 
			
		||||
    path("syncmesh/", views.SyncMeshNodeID.as_view()),
 | 
			
		||||
    path("winupdates/", views.NatsWinUpdates.as_view()),
 | 
			
		||||
    path("choco/", views.NatsChoco.as_view()),
 | 
			
		||||
    path("wmi/", views.NatsWMI.as_view()),
 | 
			
		||||
    path("offline/", views.OfflineAgents.as_view()),
 | 
			
		||||
    path("logcrash/", views.LogCrash.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ import asyncio
 | 
			
		||||
import time
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
@@ -43,11 +45,25 @@ class NatsCheckIn(APIView):
 | 
			
		||||
    permission_classes = []
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        updated = False
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if pyver.parse(request.data["version"]) > pyver.parse(agent.version):
 | 
			
		||||
            updated = True
 | 
			
		||||
        agent.version = request.data["version"]
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["version", "last_seen"])
 | 
			
		||||
 | 
			
		||||
        # change agent update pending status to completed if agent has just updated
 | 
			
		||||
        if (
 | 
			
		||||
            updated
 | 
			
		||||
            and agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
 | 
			
		||||
            last_outage = agent.agentoutages.last()
 | 
			
		||||
            last_outage.recovery_time = djangotime.now()
 | 
			
		||||
@@ -237,3 +253,48 @@ class NatsWinUpdates(APIView):
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NatsWMI(APIView):
 | 
			
		||||
 | 
			
		||||
    authentication_classes = []
 | 
			
		||||
    permission_classes = []
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        agents = Agent.objects.only(
 | 
			
		||||
            "pk", "agent_id", "version", "last_seen", "overdue_time"
 | 
			
		||||
        )
 | 
			
		||||
        online: List[str] = [
 | 
			
		||||
            i.agent_id
 | 
			
		||||
            for i in agents
 | 
			
		||||
            if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
        return Response({"agent_ids": online})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OfflineAgents(APIView):
 | 
			
		||||
    authentication_classes = []
 | 
			
		||||
    permission_classes = []
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        agents = Agent.objects.only(
 | 
			
		||||
            "pk", "agent_id", "version", "last_seen", "overdue_time"
 | 
			
		||||
        )
 | 
			
		||||
        offline: List[str] = [
 | 
			
		||||
            i.agent_id for i in agents if i.has_nats and i.status != "online"
 | 
			
		||||
        ]
 | 
			
		||||
        return Response({"agent_ids": offline})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogCrash(APIView):
 | 
			
		||||
    authentication_classes = []
 | 
			
		||||
    permission_classes = []
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
 | 
			
		||||
        logger.info(
 | 
			
		||||
            f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
 | 
			
		||||
        )
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["last_seen"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
amqp==5.0.2
 | 
			
		||||
amqp==5.0.5
 | 
			
		||||
asgiref==3.3.1
 | 
			
		||||
asyncio-nats-client==0.11.4
 | 
			
		||||
billiard==3.6.3.0
 | 
			
		||||
@@ -9,7 +9,7 @@ chardet==4.0.0
 | 
			
		||||
cryptography==3.3.1
 | 
			
		||||
decorator==4.4.2
 | 
			
		||||
Django==3.1.5
 | 
			
		||||
django-cors-headers==3.6.0
 | 
			
		||||
django-cors-headers==3.7.0
 | 
			
		||||
django-rest-knox==4.1.0
 | 
			
		||||
djangorestframework==3.12.2
 | 
			
		||||
future==0.18.2
 | 
			
		||||
@@ -21,7 +21,7 @@ packaging==20.8
 | 
			
		||||
psycopg2-binary==2.8.6
 | 
			
		||||
pycparser==2.20
 | 
			
		||||
pycryptodome==3.9.9
 | 
			
		||||
pyotp==2.4.1
 | 
			
		||||
pyotp==2.5.0
 | 
			
		||||
pyparsing==2.4.7
 | 
			
		||||
pytz==2020.5
 | 
			
		||||
qrcode==6.1
 | 
			
		||||
@@ -29,8 +29,8 @@ redis==3.5.3
 | 
			
		||||
requests==2.25.1
 | 
			
		||||
six==1.15.0
 | 
			
		||||
sqlparse==0.4.1
 | 
			
		||||
twilio==6.51.0
 | 
			
		||||
urllib3==1.26.2
 | 
			
		||||
twilio==6.51.1
 | 
			
		||||
urllib3==1.26.3
 | 
			
		||||
uWSGI==2.0.19.1
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
 
 | 
			
		||||
@@ -195,4 +195,4 @@
 | 
			
		||||
        "description": "Windows Defender Exclusions for Tactical RMM",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
]
 | 
			
		||||
@@ -195,15 +195,21 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            info = json.load(f)
 | 
			
		||||
 | 
			
		||||
        for script in info:
 | 
			
		||||
            self.assertTrue(
 | 
			
		||||
                os.path.exists(os.path.join(scripts_dir, script["filename"]))
 | 
			
		||||
            )
 | 
			
		||||
            fn: str = script["filename"]
 | 
			
		||||
            self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn)))
 | 
			
		||||
            self.assertTrue(script["filename"])
 | 
			
		||||
            self.assertTrue(script["name"])
 | 
			
		||||
            self.assertTrue(script["description"])
 | 
			
		||||
            self.assertTrue(script["shell"])
 | 
			
		||||
            self.assertIn(script["shell"], valid_shells)
 | 
			
		||||
 | 
			
		||||
            if fn.endswith(".ps1"):
 | 
			
		||||
                self.assertEqual(script["shell"], "powershell")
 | 
			
		||||
            elif fn.endswith(".bat"):
 | 
			
		||||
                self.assertEqual(script["shell"], "cmd")
 | 
			
		||||
            elif fn.endswith(".py"):
 | 
			
		||||
                self.assertEqual(script["shell"], "python")
 | 
			
		||||
 | 
			
		||||
    def test_load_community_scripts(self):
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
 | 
			
		||||
 
 | 
			
		||||
@@ -29,26 +29,10 @@ app.conf.beat_schedule = {
 | 
			
		||||
        "task": "winupdate.tasks.check_agent_update_schedule_task",
 | 
			
		||||
        "schedule": crontab(minute=5, hour="*"),
 | 
			
		||||
    },
 | 
			
		||||
    "agents-checkinfull": {
 | 
			
		||||
        "task": "agents.tasks.check_in_task",
 | 
			
		||||
        "schedule": crontab(minute="*/24"),
 | 
			
		||||
    },
 | 
			
		||||
    "agent-auto-update": {
 | 
			
		||||
        "task": "agents.tasks.auto_self_agent_update_task",
 | 
			
		||||
        "schedule": crontab(minute=35, hour="*"),
 | 
			
		||||
    },
 | 
			
		||||
    "agents-sync": {
 | 
			
		||||
        "task": "agents.tasks.sync_sysinfo_task",
 | 
			
		||||
        "schedule": crontab(minute=55, hour="*"),
 | 
			
		||||
    },
 | 
			
		||||
    "get-wmi": {
 | 
			
		||||
        "task": "agents.tasks.get_wmi_task",
 | 
			
		||||
        "schedule": crontab(minute="*/18"),
 | 
			
		||||
    },
 | 
			
		||||
    "check-agentservice": {
 | 
			
		||||
        "task": "agents.tasks.monitor_agents_task",
 | 
			
		||||
        "schedule": crontab(minute="*/15"),
 | 
			
		||||
    },
 | 
			
		||||
    "remove-salt": {
 | 
			
		||||
        "task": "agents.tasks.remove_salt_task",
 | 
			
		||||
        "schedule": crontab(minute=14, hour="*/2"),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,19 +15,19 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
 | 
			
		||||
AUTH_USER_MODEL = "accounts.User"
 | 
			
		||||
 | 
			
		||||
# latest release
 | 
			
		||||
TRMM_VERSION = "0.4.2"
 | 
			
		||||
TRMM_VERSION = "0.4.5"
 | 
			
		||||
 | 
			
		||||
# bump this version everytime vue code is changed
 | 
			
		||||
# to alert user they need to manually refresh their browser
 | 
			
		||||
APP_VER = "0.0.109"
 | 
			
		||||
APP_VER = "0.0.110"
 | 
			
		||||
 | 
			
		||||
# https://github.com/wh1te909/rmmagent
 | 
			
		||||
LATEST_AGENT_VER = "1.4.1"
 | 
			
		||||
LATEST_AGENT_VER = "1.4.2"
 | 
			
		||||
 | 
			
		||||
MESH_VER = "0.7.54"
 | 
			
		||||
 | 
			
		||||
# for the update script, bump when need to recreate venv or npm install
 | 
			
		||||
PIP_VER = "7"
 | 
			
		||||
PIP_VER = "8"
 | 
			
		||||
NPM_VER = "7"
 | 
			
		||||
 | 
			
		||||
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ jobs:
 | 
			
		||||
    displayName: "Setup"
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        Ubuntu20:
 | 
			
		||||
          AGENT_NAME: "rmm-ubu20"
 | 
			
		||||
        Debian10:
 | 
			
		||||
          AGENT_NAME: "azpipelines-deb10"
 | 
			
		||||
 | 
			
		||||
    pool:
 | 
			
		||||
      name: linux-vms
 | 
			
		||||
@@ -23,11 +23,11 @@ jobs:
 | 
			
		||||
 | 
			
		||||
          rm -rf /myagent/_work/1/s/api/env
 | 
			
		||||
          cd /myagent/_work/1/s/api
 | 
			
		||||
          python3 -m venv env
 | 
			
		||||
          python3.8 -m venv env
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd /myagent/_work/1/s/api/tacticalrmm
 | 
			
		||||
          pip install --no-cache-dir --upgrade pip
 | 
			
		||||
          pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
 | 
			
		||||
          pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
 | 
			
		||||
          pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
 | 
			
		||||
        displayName: "Install Python Dependencies"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@@ -3,14 +3,10 @@ module github.com/wh1te909/tacticalrmm
 | 
			
		||||
go 1.15
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/go-resty/resty/v2 v2.4.0
 | 
			
		||||
	github.com/josephspurrier/goversioninfo v1.2.0
 | 
			
		||||
	github.com/kr/pretty v0.1.0 // indirect
 | 
			
		||||
	github.com/nats-io/nats.go v1.10.1-0.20210107160453-a133396829fc
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.2
 | 
			
		||||
	github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.3
 | 
			
		||||
	github.com/wh1te909/rmmagent v1.4.2
 | 
			
		||||
	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
 | 
			
		||||
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 | 
			
		||||
	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								go.sum
									
									
									
									
									
								
							@@ -5,12 +5,11 @@ github.com/capnspacehook/taskmaster v0.0.0-20201022195506-c2d8b114cec0/go.mod h1
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/elastic/go-sysinfo v1.4.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
 | 
			
		||||
github.com/elastic/go-sysinfo v1.5.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
 | 
			
		||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
@@ -37,7 +36,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 | 
			
		||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
 | 
			
		||||
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
 | 
			
		||||
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
 | 
			
		||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
 | 
			
		||||
@@ -68,14 +66,15 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 | 
			
		||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 | 
			
		||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 | 
			
		||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 | 
			
		||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
 | 
			
		||||
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 | 
			
		||||
github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs=
 | 
			
		||||
github.com/rickb777/date v1.14.3/go.mod h1:mes+vf4wqTD6l4zgZh4Z5TQkrLA57dpuzEGVeTk/XSc=
 | 
			
		||||
github.com/rickb777/date v1.15.3/go.mod h1:+spwdRnUrpqbYLOmRM6y8FbQMXwpNwHrNcWuOUipge4=
 | 
			
		||||
github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
 | 
			
		||||
github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
 | 
			
		||||
github.com/shirou/gopsutil/v3 v3.20.12/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
 | 
			
		||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
@@ -83,32 +82,13 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/tc-hib/goversioninfo v0.0.0-20200813185747-90ffbaa484a7/go.mod h1:NaPIGx19A2KXQEoek0x88NbM0lNgRooZS0xmrETzcjI=
 | 
			
		||||
github.com/tc-hib/rsrc v0.9.1/go.mod h1:JGDB/TLOdMTvEEvjv3yetUTFnjXWYLbZDDeH4BTXG/8=
 | 
			
		||||
github.com/tc-hib/rsrc v0.9.2/go.mod h1:vUZqBwu0vX+ueZH/D5wEvihBZfON5BrWCg6Orbfq7A4=
 | 
			
		||||
github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc=
 | 
			
		||||
github.com/ugorji/go v1.2.2 h1:60ZHIOcsJlo3bJm9CbTVu7OSqT2mxaEmyQbK2NwCkn0=
 | 
			
		||||
github.com/ugorji/go v1.2.2/go.mod h1:bitgyERdV7L7Db/Z5gfd5v2NQMNhhiFiZwpgMw2SP7k=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.2 h1:08Gah8d+dXj4cZNUHhtuD/S4PXD5WpVbj5B8/ClELAQ=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.2/go.mod h1:OM8g7OAy52uYl3Yk+RE/3AS1nXFn1Wh4PPLtupCxbuU=
 | 
			
		||||
github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk=
 | 
			
		||||
github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc=
 | 
			
		||||
github.com/wh1te909/go-win64api v0.0.0-20201021040544-8fba2a0fc3d0/go.mod h1:cfD5/vNQFm5PD5Q32YYYBJ6VIs9etzp8CJ9dinUcpUA=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111092134-7c83da579caa h1:ZV7qIUJ5M3HDFLi3bun6a2A5+g9DoThbLWI7egBYYkQ=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111092134-7c83da579caa/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111205455-e6620a17aebe h1:xsutMbsAJL2xTvE119BVyK4RdBWx1IBvC7azoEpioEE=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210111205455-e6620a17aebe/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210112033642-9b310c2c7f53 h1:Q47sibbW09BWaQoPZQTzblGd+rnNIc3W8W/jOYbMe10=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.1.13-0.20210112033642-9b310c2c7f53/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.0 h1:dM/juD7k6Oa0lEKsvbNPgjc1wVC6uQtNzQoIqVuuxSQ=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.0/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210118235958-bd6606570a6f h1:lhcD2yJauZ8TyYCxYvSv/CPnUhiTrxwydPTESfPkyuc=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210118235958-bd6606570a6f/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119030741-08ec2f919198 h1:lPxk5AEr/2y8txGtvbQgW0rofZ7RFaJBYmS8rLIxoVQ=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119030741-08ec2f919198/go.mod h1:05MQOAiC/kGvJjDlCOjaTsMNpf6wZFqOTkHqK0ATfW0=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119225811-d3b8795ce1d7 h1:ctMUmZtlI2dH1WCndTFPOueWgYd18n+onYsnMKT/lns=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210119225811-d3b8795ce1d7/go.mod h1:TG09pCLQZcN5jyrokVty3eHImponjh5nMmifru9RPeY=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5 h1:md2uqZE2Too7mRvWCvA7vDpdpFP1bMEKWAfrIa0ARiA=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.2.2-0.20210121224121-abcecefe6da5/go.mod h1:TG09pCLQZcN5jyrokVty3eHImponjh5nMmifru9RPeY=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.4.2 h1:noG/ELSue3d6UF7o0gp1ty0DpGbfrVz0VG6NEQm9kGM=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.4.2/go.mod h1:mcI27szhAGjAQzhX8eCsluE5670kUAyi3tJVJa6LNTo=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
@@ -117,11 +97,10 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNm
 | 
			
		||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
			
		||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
			
		||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
 | 
			
		||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
 | 
			
		||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 | 
			
		||||
@@ -133,6 +112,7 @@ golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5h
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
@@ -141,9 +121,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20200622182413-4b0db7f3f76b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
@@ -169,6 +149,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="34"
 | 
			
		||||
SCRIPT_VERSION="36"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt install -y curl wget
 | 
			
		||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
 | 
			
		||||
 | 
			
		||||
GREEN='\033[0;32m'
 | 
			
		||||
YELLOW='\033[1;33m'
 | 
			
		||||
@@ -375,7 +375,7 @@ python3 -m venv env
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
pip install --no-cache-dir --upgrade pip
 | 
			
		||||
pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py migrate
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
@@ -788,6 +788,7 @@ sleep 5
 | 
			
		||||
MESHEXE=$(node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} GenerateInviteLink --group TacticalRMM --hours 8)
 | 
			
		||||
 | 
			
		||||
sudo systemctl enable nats.service
 | 
			
		||||
sudo systemctl enable natsapi.service
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
python manage.py initial_db_setup
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							@@ -9,7 +9,7 @@ import (
 | 
			
		||||
	"github.com/wh1te909/tacticalrmm/natsapi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var version = "1.0.2"
 | 
			
		||||
var version = "1.0.5"
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	ver := flag.Bool("version", false, "Prints version")
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,9 @@ func Listen(apihost, natshost, version string, debug bool) {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go getWMI(rClient, nc)
 | 
			
		||||
	go monitorAgents(rClient, nc)
 | 
			
		||||
 | 
			
		||||
	nc.Subscribe("*", func(msg *nats.Msg) {
 | 
			
		||||
		var mh codec.MsgpackHandle
 | 
			
		||||
		mh.RawToString = true
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										69
									
								
								natsapi/tasks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								natsapi/tasks.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-resty/resty/v2"
 | 
			
		||||
	nats "github.com/nats-io/nats.go"
 | 
			
		||||
	"github.com/ugorji/go/codec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func monitorAgents(c *resty.Client, nc *nats.Conn) {
 | 
			
		||||
	var payload, recPayload []byte
 | 
			
		||||
	var mh codec.MsgpackHandle
 | 
			
		||||
	mh.RawToString = true
 | 
			
		||||
	ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
 | 
			
		||||
	ret.Encode(map[string]string{"func": "ping"})
 | 
			
		||||
 | 
			
		||||
	rec := codec.NewEncoderBytes(&recPayload, new(codec.MsgpackHandle))
 | 
			
		||||
	rec.Encode(Recovery{
 | 
			
		||||
		Func: "recover",
 | 
			
		||||
		Data: map[string]string{"mode": "tacagent"},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	tick := time.NewTicker(10 * time.Minute)
 | 
			
		||||
	for range tick.C {
 | 
			
		||||
		agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/offline/")
 | 
			
		||||
		ids := agentids.Result().(*AgentIDS).IDs
 | 
			
		||||
		var resp string
 | 
			
		||||
		for _, id := range ids {
 | 
			
		||||
			out, err := nc.Request(id, payload, 2*time.Second)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			dec := codec.NewDecoderBytes(out.Data, &mh)
 | 
			
		||||
			if err := dec.Decode(&resp); err == nil {
 | 
			
		||||
				// if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
 | 
			
		||||
				// then tacticalagent service is hung. forcefully restart it
 | 
			
		||||
				if resp == "pong" {
 | 
			
		||||
					nc.Publish(id, recPayload)
 | 
			
		||||
					p := map[string]string{"agentid": id}
 | 
			
		||||
					c.R().SetBody(p).Post("/logcrash/")
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getWMI(c *resty.Client, nc *nats.Conn) {
 | 
			
		||||
	var payload []byte
 | 
			
		||||
	var mh codec.MsgpackHandle
 | 
			
		||||
	mh.RawToString = true
 | 
			
		||||
	ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
 | 
			
		||||
	ret.Encode(map[string]string{"func": "wmi"})
 | 
			
		||||
 | 
			
		||||
	tick := time.NewTicker(18 * time.Minute)
 | 
			
		||||
	for range tick.C {
 | 
			
		||||
		agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/wmi/")
 | 
			
		||||
		ids := agentids.Result().(*AgentIDS).IDs
 | 
			
		||||
		chunks := makeChunks(ids, 40)
 | 
			
		||||
 | 
			
		||||
		for _, id := range chunks {
 | 
			
		||||
			for _, chunk := range id {
 | 
			
		||||
				nc.Publish(chunk, payload)
 | 
			
		||||
				time.Sleep(time.Duration(randRange(50, 400)) * time.Millisecond)
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(15 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -4,3 +4,12 @@ type NatsInfo struct {
 | 
			
		||||
	User     string `json:"user"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AgentIDS struct {
 | 
			
		||||
	IDs []string `json:"agent_ids"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Recovery struct {
 | 
			
		||||
	Func string            `json:"func"`
 | 
			
		||||
	Data map[string]string `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								natsapi/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								natsapi/utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func makeChunks(ids []string, chunkSize int) [][]string {
 | 
			
		||||
	var chunks [][]string
 | 
			
		||||
	for i := 0; i < len(ids); i += chunkSize {
 | 
			
		||||
		end := i + chunkSize
 | 
			
		||||
		if end > len(ids) {
 | 
			
		||||
			end = len(ids)
 | 
			
		||||
		}
 | 
			
		||||
		chunks = append(chunks, ids[i:end])
 | 
			
		||||
	}
 | 
			
		||||
	return chunks
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func randRange(min, max int) int {
 | 
			
		||||
	rand.Seed(time.Now().UnixNano())
 | 
			
		||||
	return rand.Intn(max-min) + min
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,7 @@ pgpw="hunter2"
 | 
			
		||||
 | 
			
		||||
#####################################################
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="13"
 | 
			
		||||
SCRIPT_VERSION="14"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt install -y curl wget
 | 
			
		||||
@@ -246,7 +246,7 @@ python3 -m venv env
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
pip install --no-cache-dir --upgrade pip
 | 
			
		||||
pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
python manage.py reload_nats
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="104"
 | 
			
		||||
SCRIPT_VERSION="105"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh'
 | 
			
		||||
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
 | 
			
		||||
YELLOW='\033[1;33m'
 | 
			
		||||
@@ -165,6 +165,9 @@ printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
 | 
			
		||||
sudo systemctl stop ${i}
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
# forgot to add this in install script. catch any installs that don't have it enabled and enable it
 | 
			
		||||
sudo systemctl enable natsapi.service
 | 
			
		||||
 | 
			
		||||
CHECK_NGINX_WORKER_CONN=$(grep "worker_connections 2048" /etc/nginx/nginx.conf)
 | 
			
		||||
if ! [[ $CHECK_NGINX_WORKER_CONN ]]; then
 | 
			
		||||
  printf >&2 "${GREEN}Changing nginx worker connections to 2048${NC}\n"
 | 
			
		||||
@@ -242,7 +245,7 @@ if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then
 | 
			
		||||
  source /rmm/api/env/bin/activate
 | 
			
		||||
  cd /rmm/api/tacticalrmm
 | 
			
		||||
  pip install --no-cache-dir --upgrade pip
 | 
			
		||||
  pip install --no-cache-dir setuptools==51.1.2 wheel==0.36.2
 | 
			
		||||
  pip install --no-cache-dir setuptools==52.0.0 wheel==0.36.2
 | 
			
		||||
  pip install --no-cache-dir -r requirements.txt
 | 
			
		||||
else
 | 
			
		||||
  source /rmm/api/env/bin/activate
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,6 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
export default {
 | 
			
		||||
  name: "UpdateAgents",
 | 
			
		||||
@@ -58,7 +57,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    getVersions() {
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
      axios
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .get("/agents/getagentversions/")
 | 
			
		||||
        .then(r => {
 | 
			
		||||
          this.versions = r.data.versions;
 | 
			
		||||
@@ -72,8 +71,8 @@ export default {
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    update() {
 | 
			
		||||
      const data = { version: this.version, pks: this.group };
 | 
			
		||||
      axios
 | 
			
		||||
      const data = { pks: this.group };
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .post("/agents/updateagents/", data)
 | 
			
		||||
        .then(r => {
 | 
			
		||||
          this.$emit("close");
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user