Compare commits
	
		
			46 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 1e03c628d5 | ||
|  | 71fb39db1f | ||
|  | bcfb3726b0 | ||
|  | c6e9e29671 | ||
|  | 1bfefcce39 | ||
|  | 22488e93e1 | ||
|  | 244b89f035 | ||
|  | 1f9a241b94 | ||
|  | 03641aae42 | ||
|  | a2bdd113cc | ||
|  | a92e2f3c7b | ||
|  | 97766b3a57 | ||
|  | 9ef4c3bb06 | ||
|  | d82f0cd757 | ||
|  | 5f529e2af4 | ||
|  | beadd9e02b | ||
|  | 72543789cb | ||
|  | 5789439fa9 | ||
|  | f549126bcf | ||
|  | 7197548bad | ||
|  | 241fde783c | ||
|  | 2b872cd1f4 | ||
|  | a606fb4d1d | ||
|  | 9f9c6be38e | ||
|  | 01ee524049 | ||
|  | af9cb65338 | ||
|  | 8aa11c580b | ||
|  | ada627f444 | ||
|  | a7b6d338c3 | ||
|  | 9f00538b97 | ||
|  | a085015282 | ||
|  | 0b9c220fbb | ||
|  | 0e3d04873d | ||
|  | b7578d939f | ||
|  | b5c28de03f | ||
|  | e17d25c156 | ||
|  | c25dc1b99c | ||
|  | a493a574bd | ||
|  | 4284493dce | ||
|  | 25059de8e1 | ||
|  | 1731b05ad0 | ||
|  | e80dc663ac | ||
|  | 39988a4c2f | ||
|  | 415bff303a | ||
|  | a65eb62a54 | ||
|  | 03b2982128 | 
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,12 @@ class AgentSerializer(serializers.ModelSerializer): | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class AgentOverdueActionSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = ["pk", "overdue_email_alert", "overdue_text_alert"] | ||||
|  | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     pending_actions = serializers.SerializerMethodField() | ||||
|   | ||||
| @@ -16,50 +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 r == "pong": | ||||
|         logger.info( | ||||
|             f"Detected crashed tacticalagent service on {agent.hostname}, 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 | ||||
| @@ -75,51 +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" | ||||
|  | ||||
|     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) | ||||
| @@ -148,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) | ||||
|  | ||||
| @@ -479,42 +530,20 @@ class TestAgentViews(TacticalTestCase): | ||||
|     def test_overdue_action(self): | ||||
|         url = "/agents/overdueaction/" | ||||
|  | ||||
|         payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"} | ||||
|         payload = {"pk": self.agent.pk, "overdue_email_alert": True} | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertTrue(agent.overdue_email_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "email", "action": "disabled"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertFalse(agent.overdue_email_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "enabled"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertTrue(agent.overdue_text_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "disabled"}) | ||||
|         payload = {"pk": self.agent.pk, "overdue_text_alert": False} | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         agent = Agent.objects.get(pk=self.agent.pk) | ||||
|         self.assertFalse(agent.overdue_text_alert) | ||||
|         self.assertEqual(self.agent.hostname, r.data) | ||||
|  | ||||
|         payload.update({"alertType": "email", "action": "523423"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         payload.update({"alertType": "text", "action": "asdasd3434asdasd"}) | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_list_agents_no_detail(self): | ||||
| @@ -758,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( | ||||
| @@ -786,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", | ||||
| @@ -806,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) | ||||
|   | ||||
| @@ -30,6 +30,7 @@ from .serializers import ( | ||||
|     AgentEditSerializer, | ||||
|     NoteSerializer, | ||||
|     NotesSerializer, | ||||
|     AgentOverdueActionSerializer, | ||||
| ) | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| @@ -58,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") | ||||
|  | ||||
|  | ||||
| @@ -183,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") | ||||
|  | ||||
| @@ -333,26 +339,12 @@ def by_site(request, sitepk): | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def overdue_action(request): | ||||
|     pk = request.data["pk"] | ||||
|     alert_type = request.data["alertType"] | ||||
|     action = request.data["action"] | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     if alert_type == "email" and action == "enabled": | ||||
|         agent.overdue_email_alert = True | ||||
|         agent.save(update_fields=["overdue_email_alert"]) | ||||
|     elif alert_type == "email" and action == "disabled": | ||||
|         agent.overdue_email_alert = False | ||||
|         agent.save(update_fields=["overdue_email_alert"]) | ||||
|     elif alert_type == "text" and action == "enabled": | ||||
|         agent.overdue_text_alert = True | ||||
|         agent.save(update_fields=["overdue_text_alert"]) | ||||
|     elif alert_type == "text" and action == "disabled": | ||||
|         agent.overdue_text_alert = False | ||||
|         agent.save(update_fields=["overdue_text_alert"]) | ||||
|     else: | ||||
|         return Response( | ||||
|             {"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST | ||||
|         ) | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|     serializer = AgentOverdueActionSerializer( | ||||
|         instance=agent, data=request.data, partial=True | ||||
|     ) | ||||
|     serializer.is_valid(raise_exception=True) | ||||
|     serializer.save() | ||||
|     return Response(agent.hostname) | ||||
|  | ||||
|  | ||||
| @@ -473,7 +465,7 @@ def install_agent(request): | ||||
|             f"GOARCH={goarch}", | ||||
|             go_bin, | ||||
|             "build", | ||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", | ||||
|             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||
|             f"-X 'main.Api={api}'", | ||||
|             f"-X 'main.Client={client_id}'", | ||||
|             f"-X 'main.Site={site_id}'", | ||||
|   | ||||
| @@ -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()), | ||||
| ] | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from autotasks.models import AutomatedTask | ||||
| from accounts.models import User | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from software.models import InstalledSoftware | ||||
| from checks.serializers import CheckRunnerGetSerializerV3 | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from agents.serializers import WinAgentSerializer | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||
| from winupdate.serializers import ApprovedUpdateSerializer | ||||
| @@ -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 | ||||
| @@ -232,7 +51,7 @@ class CheckRunner(APIView): | ||||
|         ret = { | ||||
|             "agent": agent.pk, | ||||
|             "check_interval": agent.check_interval, | ||||
|             "checks": CheckRunnerGetSerializerV3(checks, many=True).data, | ||||
|             "checks": CheckRunnerGetSerializer(checks, many=True).data, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
| @@ -292,74 +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"]) | ||||
|  | ||||
|         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." | ||||
|                 ) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SysInfo(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
| @@ -375,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 """ | ||||
|  | ||||
|   | ||||
| @@ -445,42 +445,6 @@ class Check(BaseAuditModel): | ||||
|  | ||||
|         return self.status | ||||
|  | ||||
|     def handle_check(self, data): | ||||
|         if self.check_type != "cpuload" and self.check_type != "memory": | ||||
|  | ||||
|             if data["status"] == "passing" and self.fail_count != 0: | ||||
|                 self.fail_count = 0 | ||||
|                 self.save(update_fields=["fail_count"]) | ||||
|  | ||||
|             elif data["status"] == "failing": | ||||
|                 self.fail_count += 1 | ||||
|                 self.save(update_fields=["fail_count"]) | ||||
|  | ||||
|         else: | ||||
|             self.history.append(data["percent"]) | ||||
|  | ||||
|             if len(self.history) > 15: | ||||
|                 self.history = self.history[-15:] | ||||
|  | ||||
|             self.save(update_fields=["history"]) | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|  | ||||
|             if avg > self.threshold: | ||||
|                 self.status = "failing" | ||||
|                 self.fail_count += 1 | ||||
|                 self.save(update_fields=["status", "fail_count"]) | ||||
|             else: | ||||
|                 self.status = "passing" | ||||
|                 if self.fail_count != 0: | ||||
|                     self.fail_count = 0 | ||||
|                     self.save(update_fields=["status", "fail_count"]) | ||||
|                 else: | ||||
|                     self.save(update_fields=["status"]) | ||||
|  | ||||
|         if self.email_alert and self.fail_count >= self.fails_b4_alert: | ||||
|             handle_check_email_alert_task.delay(self.pk) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(check): | ||||
|         # serializes the check and returns json | ||||
|   | ||||
| @@ -95,101 +95,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializer(serializers.ModelSerializer): | ||||
|     # for the windows agent | ||||
|     # only send data needed for agent to run a check | ||||
|  | ||||
|     assigned_task = serializers.SerializerMethodField() | ||||
|     script = ScriptSerializer(read_only=True) | ||||
|  | ||||
|     def get_assigned_task(self, obj): | ||||
|         if obj.assignedtask.exists(): | ||||
|             # this will not break agents on version 0.10.2 or lower | ||||
|             # newer agents once released will properly handle multiple tasks assigned to a check | ||||
|             task = obj.assignedtask.first() | ||||
|             return AssignedTaskCheckRunnerField(task).data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Check | ||||
|         exclude = [ | ||||
|             "policy", | ||||
|             "managed_by_policy", | ||||
|             "overriden_by_policy", | ||||
|             "parent_check", | ||||
|             "name", | ||||
|             "more_info", | ||||
|             "last_run", | ||||
|             "email_alert", | ||||
|             "text_alert", | ||||
|             "fails_b4_alert", | ||||
|             "fail_count", | ||||
|             "email_sent", | ||||
|             "text_sent", | ||||
|             "outage_history", | ||||
|             "extra_details", | ||||
|             "stdout", | ||||
|             "stderr", | ||||
|             "retcode", | ||||
|             "execution_time", | ||||
|             "svc_display_name", | ||||
|             "svc_policy_mode", | ||||
|             "created_by", | ||||
|             "created_time", | ||||
|             "modified_by", | ||||
|             "modified_time", | ||||
|             "history", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializerV2(serializers.ModelSerializer): | ||||
|     # for the windows __python__ agent | ||||
|     # only send data needed for agent to run a check | ||||
|  | ||||
|     assigned_tasks = serializers.SerializerMethodField() | ||||
|     script = ScriptSerializer(read_only=True) | ||||
|  | ||||
|     def get_assigned_tasks(self, obj): | ||||
|         if obj.assignedtask.exists(): | ||||
|             tasks = obj.assignedtask.all() | ||||
|             return AssignedTaskCheckRunnerField(tasks, many=True).data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Check | ||||
|         exclude = [ | ||||
|             "policy", | ||||
|             "managed_by_policy", | ||||
|             "overriden_by_policy", | ||||
|             "parent_check", | ||||
|             "name", | ||||
|             "more_info", | ||||
|             "last_run", | ||||
|             "email_alert", | ||||
|             "text_alert", | ||||
|             "fails_b4_alert", | ||||
|             "fail_count", | ||||
|             "email_sent", | ||||
|             "text_sent", | ||||
|             "outage_history", | ||||
|             "extra_details", | ||||
|             "stdout", | ||||
|             "stderr", | ||||
|             "retcode", | ||||
|             "execution_time", | ||||
|             "svc_display_name", | ||||
|             "svc_policy_mode", | ||||
|             "created_by", | ||||
|             "created_time", | ||||
|             "modified_by", | ||||
|             "modified_time", | ||||
|             "history", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class CheckRunnerGetSerializerV3(serializers.ModelSerializer): | ||||
|     # for the windows __golang__ agent | ||||
|     # only send data needed for agent to run a check | ||||
|     # the difference here is in the script serializer | ||||
|     # script checks no longer rely on salt and are executed directly by the go agent | ||||
|  | ||||
|     assigned_tasks = serializers.SerializerMethodField() | ||||
|     script = ScriptCheckSerializer(read_only=True) | ||||
|  | ||||
|   | ||||
| @@ -2,9 +2,9 @@ from checks.models import CheckHistory | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from .serializers import CheckSerializer | ||||
| from django.utils import timezone as djangotime | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker | ||||
| from itertools import cycle | ||||
|  | ||||
|  | ||||
| class TestCheckViews(TacticalTestCase): | ||||
| @@ -184,6 +184,48 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("patch", url_a) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_run_checks(self, nats_cmd): | ||||
|         agent = baker.make_recipe("agents.agent", version="1.4.1") | ||||
|         agent_old = baker.make_recipe("agents.agent", version="1.0.2") | ||||
|         agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0") | ||||
|  | ||||
|         url = f"/checks/runchecks/{agent_old.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|         url = f"/checks/runchecks/{agent_b4_141.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, wait=False) | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "busy" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}") | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "ok" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}") | ||||
|  | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.return_value = "timeout" | ||||
|         url = f"/checks/runchecks/{agent.pk}/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) | ||||
|         self.assertEqual(r.json(), "Unable to contact the agent") | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_get_check_history(self): | ||||
|         # setup data | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import asyncio | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.db.models import Q | ||||
| @@ -168,8 +169,17 @@ def run_checks(request, pk): | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|     asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) | ||||
|     return Response(agent.hostname) | ||||
|     if pyver.parse(agent.version) >= pyver.parse("1.4.1"): | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15)) | ||||
|         if r == "busy": | ||||
|             return notify_error(f"Checks are already running on {agent.hostname}") | ||||
|         elif r == "ok": | ||||
|             return Response(f"Checks will now be re-run on {agent.hostname}") | ||||
|         else: | ||||
|             return notify_error("Unable to contact the agent") | ||||
|     else: | ||||
|         asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) | ||||
|         return Response(f"Checks will now be re-run on {agent.hostname}") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
|   | ||||
| @@ -223,7 +223,7 @@ class GenerateAgent(APIView): | ||||
|             f"GOARCH={goarch}", | ||||
|             go_bin, | ||||
|             "build", | ||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", | ||||
|             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||
|             f"-X 'main.Api={api}'", | ||||
|             f"-X 'main.Client={d.client.pk}'", | ||||
|             f"-X 'main.Site={d.site.pk}'", | ||||
|   | ||||
							
								
								
									
										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,8 @@ 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()), | ||||
|     path("superseded/", views.SupersededWinUpdate.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,29 @@ 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 | ||||
|         ) or pyver.parse(request.data["version"]) == pyver.parse( | ||||
|             settings.LATEST_AGENT_VER | ||||
|         ): | ||||
|             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() | ||||
| @@ -176,6 +196,7 @@ class NatsWinUpdates(APIView): | ||||
|             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") | ||||
|  | ||||
|     def patch(self, request): | ||||
| @@ -199,6 +220,7 @@ class NatsWinUpdates(APIView): | ||||
|             u.result = "failed" | ||||
|             u.save(update_fields=["result"]) | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
| @@ -233,4 +255,71 @@ class NatsWinUpdates(APIView): | ||||
|                     revision_number=update["revision_number"], | ||||
|                 ).save() | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|  | ||||
|         # more superseded updates cleanup | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.4.2"): | ||||
|             for u in agent.winupdates.filter( | ||||
|                 date_installed__isnull=True, result="failed" | ||||
|             ).exclude(installed=True): | ||||
|                 u.delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SupersededWinUpdate(APIView): | ||||
|     authentication_classes = [] | ||||
|     permission_classes = [] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         updates = agent.winupdates.filter(guid=request.data["guid"]) | ||||
|         for u in updates: | ||||
|             u.delete() | ||||
|  | ||||
|         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 | ||||
|   | ||||
| @@ -193,6 +193,6 @@ | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "TRMM Defender Exclusions", | ||||
|         "description": "Windows Defender Exclusions for Tactical RMM", | ||||
|         "shell": "cmd" | ||||
|         "shell": "powershell" | ||||
|     } | ||||
| ] | ||||
| @@ -72,6 +72,7 @@ class Script(BaseAuditModel): | ||||
|                     i.name = script["name"] | ||||
|                     i.description = script["description"] | ||||
|                     i.category = "Community" | ||||
|                     i.shell = script["shell"] | ||||
|  | ||||
|                     with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: | ||||
|                         script_bytes = ( | ||||
| @@ -80,7 +81,13 @@ class Script(BaseAuditModel): | ||||
|                         i.code_base64 = base64.b64encode(script_bytes).decode("ascii") | ||||
|  | ||||
|                     i.save( | ||||
|                         update_fields=["name", "description", "category", "code_base64"] | ||||
|                         update_fields=[ | ||||
|                             "name", | ||||
|                             "description", | ||||
|                             "category", | ||||
|                             "code_base64", | ||||
|                             "shell", | ||||
|                         ] | ||||
|                     ) | ||||
|                 else: | ||||
|                     print(f"Adding new community script: {script['name']}") | ||||
|   | ||||
| @@ -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.1" | ||||
| TRMM_VERSION = "0.4.7" | ||||
|  | ||||
| # bump this version everytime vue code is changed | ||||
| # to alert user they need to manually refresh their browser | ||||
| APP_VER = "0.0.108" | ||||
| APP_VER = "0.0.111" | ||||
|  | ||||
| # https://github.com/wh1te909/rmmagent | ||||
| LATEST_AGENT_VER = "1.4.0" | ||||
| LATEST_AGENT_VER = "1.4.4" | ||||
|  | ||||
| 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" | ||||
|   | ||||
| @@ -21,6 +21,7 @@ def auto_approve_updates_task(): | ||||
|  | ||||
|     agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time") | ||||
|     for agent in agents: | ||||
|         agent.delete_superseded_updates() | ||||
|         try: | ||||
|             agent.approve_updates() | ||||
|         except: | ||||
| @@ -53,6 +54,7 @@ def check_agent_update_schedule_task(): | ||||
|     ] | ||||
|  | ||||
|     for agent in online: | ||||
|         agent.delete_superseded_updates() | ||||
|         install = False | ||||
|         patch_policy = agent.get_patch_policy() | ||||
|  | ||||
| @@ -126,6 +128,11 @@ def bulk_install_updates_task(pks: List[int]) -> None: | ||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             agent.delete_superseded_updates() | ||||
|             try: | ||||
|                 agent.approve_updates() | ||||
|             except: | ||||
|                 pass | ||||
|             nats_data = { | ||||
|                 "func": "installwinupdates", | ||||
|                 "guids": agent.get_approved_update_guids(), | ||||
| @@ -142,6 +149,7 @@ def bulk_check_for_updates_task(pks: List[int]) -> None: | ||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             agent.delete_superseded_updates() | ||||
|             asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) | ||||
|             time.sleep(0.05) | ||||
|         time.sleep(15) | ||||
|   | ||||
| @@ -22,6 +22,7 @@ def get_win_updates(request, pk): | ||||
| @api_view() | ||||
| def run_update_scan(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     agent.delete_superseded_updates() | ||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||
|         return notify_error("Requires agent version 1.3.0 or greater") | ||||
|  | ||||
| @@ -32,9 +33,11 @@ def run_update_scan(request, pk): | ||||
| @api_view() | ||||
| def install_updates(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     agent.delete_superseded_updates() | ||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||
|         return notify_error("Requires agent version 1.3.0 or greater") | ||||
|  | ||||
|     agent.approve_updates() | ||||
|     nats_data = { | ||||
|         "func": "installwinupdates", | ||||
|         "guids": agent.get_approved_update_guids(), | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
| @@ -44,7 +44,7 @@ jobs: | ||||
|       - script: | | ||||
|           cd /myagent/_work/1/s/api | ||||
|           source env/bin/activate | ||||
|           black --check tacticalrmm | ||||
|           black --exclude migrations/ --check tacticalrmm | ||||
|           if [ $? -ne 0 ]; then | ||||
|               exit 1 | ||||
|           fi | ||||
|   | ||||
							
								
								
									
										18
									
								
								backup.sh
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								backup.sh
									
									
									
									
									
								
							| @@ -1,6 +1,13 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="7" | ||||
| ##################################################### | ||||
|  | ||||
| POSTGRES_USER="changeme" | ||||
| POSTGRES_PW="hunter2" | ||||
|  | ||||
| ##################################################### | ||||
|  | ||||
| SCRIPT_VERSION="8" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| @@ -24,13 +31,6 @@ if [ $EUID -eq 0 ]; then | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| ##################################################### | ||||
|  | ||||
| POSTGRES_USER="changeme" | ||||
| POSTGRES_PW="hunter2" | ||||
|  | ||||
| ##################################################### | ||||
|  | ||||
| if [[ "$POSTGRES_USER" == "changeme" || "$POSTGRES_PW" == "hunter2" ]]; then | ||||
|   printf >&2 "${RED}You must change the postgres username/password at the top of this file.${NC}\n" | ||||
|   printf >&2 "${RED}Check the github readme for where to find them.${NC}\n" | ||||
| @@ -43,7 +43,7 @@ if [ ! -d /rmmbackups ]; then | ||||
| fi | ||||
|  | ||||
| if [ -d /meshcentral/meshcentral-backup ]; then | ||||
|     rm -f /meshcentral/meshcentral-backup/* | ||||
|     rm -rf /meshcentral/meshcentral-backup/* | ||||
| fi | ||||
|  | ||||
| if [ -d /meshcentral/meshcentral-coredumps ]; then | ||||
|   | ||||
							
								
								
									
										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.3 | ||||
| 	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 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										57
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								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,17 @@ 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= | ||||
| github.com/wh1te909/rmmagent v1.4.3-0.20210202083714-12142d6dfd1f h1:9r7AFleOMiVs8AiAsUezau3GVL22bqbUDz6qlRUIMFM= | ||||
| github.com/wh1te909/rmmagent v1.4.3-0.20210202083714-12142d6dfd1f/go.mod h1:mcI27szhAGjAQzhX8eCsluE5670kUAyi3tJVJa6LNTo= | ||||
| github.com/wh1te909/rmmagent v1.4.3 h1:9jbze6oqUBe8HJedXTrZ5i7C8dDEQt+Ifmp9am4HtZQ= | ||||
| github.com/wh1te909/rmmagent v1.4.3/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 +101,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 +116,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 +125,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 +153,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= | ||||
|   | ||||
							
								
								
									
										13
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								install.sh
									
									
									
									
									
								
							| @@ -1,9 +1,9 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="34" | ||||
| SCRIPT_VERSION="37" | ||||
| 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 | ||||
| @@ -398,19 +398,17 @@ read -n 1 -s -r -p "Press any key to continue..." | ||||
|  | ||||
| uwsgini="$(cat << EOF | ||||
| [uwsgi] | ||||
|  | ||||
| # logto = /rmm/api/tacticalrmm/tacticalrmm/private/log/uwsgi.log | ||||
| chdir = /rmm/api/tacticalrmm | ||||
| module = tacticalrmm.wsgi | ||||
| home = /rmm/api/env | ||||
| master = true | ||||
| processes = 6 | ||||
| threads = 6 | ||||
| enable-threads = True | ||||
| enable-threads = true | ||||
| socket = /rmm/api/tacticalrmm/tacticalrmm.sock | ||||
| harakiri = 300 | ||||
| chmod-socket = 660 | ||||
| # clear environment on exit | ||||
| buffer-size = 65535 | ||||
| vacuum = true | ||||
| die-on-term = true | ||||
| max-requests = 500 | ||||
| @@ -788,6 +786,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.6" | ||||
|  | ||||
| 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 | ||||
| @@ -158,6 +161,13 @@ func Listen(apihost, natshost, version string, debug bool) { | ||||
| 					rClient.R().SetBody(p).Patch("/winupdates/") | ||||
| 				} | ||||
| 			}() | ||||
| 		case "superseded": | ||||
| 			go func() { | ||||
| 				var p *rmm.SupersededUpdate | ||||
| 				if err := dec.Decode(&p); err == nil { | ||||
| 					rClient.R().SetBody(p).Post("/superseded/") | ||||
| 				} | ||||
| 			}() | ||||
| 		case "needsreboot": | ||||
| 			go func() { | ||||
| 				var p *rmm.AgentNeedsReboot | ||||
|   | ||||
										
											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 | ||||
| } | ||||
							
								
								
									
										46
									
								
								restore.sh
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								restore.sh
									
									
									
									
									
								
							| @@ -7,10 +7,10 @@ pgpw="hunter2" | ||||
|  | ||||
| ##################################################### | ||||
|  | ||||
| SCRIPT_VERSION="13" | ||||
| SCRIPT_VERSION="15" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.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' | ||||
| @@ -37,12 +37,38 @@ if [[ "$pgusername" == "changeme" || "$pgpw" == "hunter2" ]]; then | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| UBU20=$(grep 20.04 "/etc/"*"release") | ||||
| if ! [[ $UBU20 ]]; then | ||||
|   echo -ne "\033[0;31mThis restore script will only work on Ubuntu 20.04\e[0m\n" | ||||
|   exit 1 | ||||
| osname=$(lsb_release -si); osname=${osname^} | ||||
| osname=$(echo "$osname" | tr  '[A-Z]' '[a-z]') | ||||
| fullrel=$(lsb_release -sd) | ||||
| codename=$(lsb_release -sc) | ||||
| relno=$(lsb_release -sr | cut -d. -f1) | ||||
| fullrelno=$(lsb_release -sr) | ||||
|  | ||||
| # Fallback if lsb_release -si returns anything else than Ubuntu, Debian or Raspbian | ||||
| if [ ! "$osname" = "ubuntu" ] && [ ! "$osname" = "debian" ]; then | ||||
|   osname=$(grep -oP '(?<=^ID=).+' /etc/os-release | tr -d '"') | ||||
|   osname=${osname^} | ||||
| fi | ||||
|  | ||||
| # determine system | ||||
| if ([ "$osname" = "ubuntu" ] && [ "$fullrelno" = "20.04" ]) || ([ "$osname" = "debian" ] && [ $relno -ge 10 ]); then | ||||
|   echo $fullrel | ||||
| else | ||||
|  echo $fullrel | ||||
|  echo -ne "${RED}Only Ubuntu release 20.04 and Debian 10 and later, are supported\n" | ||||
|  echo -ne "Your system does not appear to be supported${NC}\n" | ||||
|  exit 1 | ||||
| fi | ||||
|  | ||||
| if ([ "$osname" = "ubuntu" ]); then | ||||
|   mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 multiverse" | ||||
| else | ||||
|   mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 main" | ||||
|  | ||||
| fi | ||||
|  | ||||
| postgresql_repo="deb [arch=amd64] https://apt.postgresql.org/pub/repos/apt/ $codename-pgdg main" | ||||
|  | ||||
| if [ $EUID -eq 0 ]; then | ||||
|   echo -ne "\033[0;31mDo NOT run this script as root. Exiting.\e[0m\n" | ||||
|   exit 1 | ||||
| @@ -172,7 +198,7 @@ sudo apt install -y python3-venv python3-dev python3-pip python3-setuptools pyth | ||||
|  | ||||
| print_green 'Installing postgresql' | ||||
|  | ||||
| sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' | ||||
| echo "$postgresql_repo" | sudo tee /etc/apt/sources.list.d/pgdg.list | ||||
| wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - | ||||
| sudo apt update | ||||
| sudo apt install -y postgresql-13 | ||||
| @@ -194,8 +220,8 @@ PGPASSWORD=${pgpw} psql -h localhost -U ${pgusername} -d tacticalrmm -f $tmp_dir | ||||
|  | ||||
| print_green 'Restoring MongoDB' | ||||
|  | ||||
| wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add - | ||||
| echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list | ||||
| wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add - | ||||
| echo "$mongodb_repo" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list | ||||
| sudo apt update | ||||
| sudo apt install -y mongodb-org | ||||
| sudo systemctl enable mongod | ||||
| @@ -246,7 +272,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 | ||||
|   | ||||
							
								
								
									
										16
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								update.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="104" | ||||
| SCRIPT_VERSION="106" | ||||
| 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" | ||||
| @@ -190,6 +193,15 @@ sudo chown -R $USER:$GROUP /home/${USER}/.cache | ||||
| sudo chown ${USER}:${USER} -R /etc/letsencrypt | ||||
| sudo chmod 775 -R /etc/letsencrypt | ||||
|  | ||||
| CHECK_BUFF_SIZE=$(grep buffer-size /rmm/api/tacticalrmm/app.ini) | ||||
| if ! [[ $CHECK_BUFF_SIZE ]]; then | ||||
| setbuff="$(cat << EOF | ||||
| buffer-size = 65535 | ||||
| EOF | ||||
| )" | ||||
| echo "${setbuff}" | tee --append /rmm/api/tacticalrmm/app.ini > /dev/null | ||||
| fi | ||||
|  | ||||
| CHECK_REMOVE_SALT=$(grep KEEP_SALT /rmm/api/tacticalrmm/tacticalrmm/local_settings.py) | ||||
| if ! [[ $CHECK_REMOVE_SALT ]]; then | ||||
|   printf >&2 "${YELLOW}This update removes salt from the rmm${NC}\n" | ||||
| @@ -242,7 +254,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 | ||||
|   | ||||
| @@ -541,10 +541,17 @@ export default { | ||||
|       window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826"); | ||||
|     }, | ||||
|     runChecks(pk) { | ||||
|       axios | ||||
|       this.$q.loading.show(); | ||||
|       this.$axios | ||||
|         .get(`/checks/runchecks/${pk}/`) | ||||
|         .then(r => this.notifySuccess(`Checks will now be re-run on ${r.data}`)) | ||||
|         .catch(e => this.notifyError(e.response.data)); | ||||
|         .then(r => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifySuccess(r.data); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifyError(e.response.data); | ||||
|         }); | ||||
|     }, | ||||
|     removeAgent(pk, name) { | ||||
|       this.$q | ||||
| @@ -637,14 +644,14 @@ export default { | ||||
|       this.$store.dispatch("loadNotes", pk); | ||||
|     }, | ||||
|     overdueAlert(category, pk, alert_action) { | ||||
|       const db_field = category === "email" ? "overdue_email_alert" : "overdue_text_alert"; | ||||
|       const action = alert_action ? "enabled" : "disabled"; | ||||
|       const data = { | ||||
|         pk: pk, | ||||
|         alertType: category, | ||||
|         action: action, | ||||
|         [db_field]: alert_action, | ||||
|       }; | ||||
|       const alertColor = alert_action ? "positive" : "warning"; | ||||
|       axios | ||||
|       this.$axios | ||||
|         .post("/agents/overdueaction/", data) | ||||
|         .then(r => { | ||||
|           this.$q.notify({ | ||||
| @@ -653,7 +660,7 @@ export default { | ||||
|             message: `Overdue ${category} alerts ${action} on ${r.data}`, | ||||
|           }); | ||||
|         }) | ||||
|         .catch(e => this.notifyError(e.response.data.error)); | ||||
|         .catch(() => this.notifyError("Something went wrong")); | ||||
|     }, | ||||
|     agentClass(status) { | ||||
|       if (status === "offline") { | ||||
|   | ||||
| @@ -68,7 +68,11 @@ | ||||
|           </q-td> | ||||
|           <q-td>{{ props.row.severity }}</q-td> | ||||
|           <q-td>{{ formatMessage(props.row.title) }}</q-td> | ||||
|           <q-td @click.native="showFullMsg(props.row.title, props.row.description)"> | ||||
|           <q-td | ||||
|             @click.native=" | ||||
|               showFullMsg(props.row.title, props.row.description, props.row.more_info_urls, props.row.categories) | ||||
|             " | ||||
|           > | ||||
|             <span style="cursor: pointer; text-decoration: underline" class="text-primary">{{ | ||||
|               formatMessage(props.row.description) | ||||
|             }}</span> | ||||
| @@ -125,7 +129,7 @@ export default { | ||||
|         }, | ||||
|         { | ||||
|           name: "description", | ||||
|           label: "Description", | ||||
|           label: "More Info", | ||||
|           field: "description", | ||||
|           align: "left", | ||||
|           sortable: true, | ||||
| @@ -137,6 +141,14 @@ export default { | ||||
|           align: "left", | ||||
|           sortable: true, | ||||
|         }, | ||||
|         { | ||||
|           name: "more_info_urls", | ||||
|           field: "more_info_urls", | ||||
|         }, | ||||
|         { | ||||
|           name: "categories", | ||||
|           field: "categories", | ||||
|         }, | ||||
|       ], | ||||
|       visibleColumns: ["action", "installed", "severity", "title", "description", "date_installed"], | ||||
|     }; | ||||
| @@ -155,10 +167,19 @@ export default { | ||||
|     formatMessage(msg) { | ||||
|       return msg.substring(0, 80) + "..."; | ||||
|     }, | ||||
|     showFullMsg(title, msg) { | ||||
|     showFullMsg(title, msg, urls, categories) { | ||||
|       let support_urls = ""; | ||||
|       urls.forEach(u => { | ||||
|         support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`; | ||||
|       }); | ||||
|       let cats = categories.join(", "); | ||||
|       this.$q.dialog({ | ||||
|         title: title, | ||||
|         message: msg.split(". ").join(".<br />"), | ||||
|         message: | ||||
|           `<b>Categories:</b> ${cats}<br/><br/>` + | ||||
|           "<b>Description</b><br/>" + | ||||
|           msg.split(". ").join(".<br />") + | ||||
|           `<br/><br/><b>Support Urls</b><br/>${support_urls}`, | ||||
|         html: true, | ||||
|         fullWidth: true, | ||||
|       }); | ||||
|   | ||||
| @@ -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