Compare commits
	
		
			25 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b5c28de03f | ||
|  | e17d25c156 | ||
|  | c25dc1b99c | ||
|  | a493a574bd | ||
|  | 4284493dce | ||
|  | 25059de8e1 | ||
|  | 1731b05ad0 | ||
|  | e80dc663ac | ||
|  | 39988a4c2f | ||
|  | 415bff303a | ||
|  | a65eb62a54 | ||
|  | 03b2982128 | ||
|  | bff0527857 | ||
|  | f3b7634254 | ||
|  | 6a9593c0b9 | ||
|  | edb785b8e5 | ||
|  | 26d757b50a | ||
|  | 535079ee87 | ||
|  | ac380c29c1 | ||
|  | 3fd212f26c | ||
|  | 04a3abc651 | ||
|  | 6caf85ddd1 | ||
|  | 16e4071508 | ||
|  | 69e7c4324b | ||
|  | a1c4a8cbe5 | 
| @@ -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): | class AgentTableSerializer(serializers.ModelSerializer): | ||||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") |     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||||
|     pending_actions = serializers.SerializerMethodField() |     pending_actions = serializers.SerializerMethodField() | ||||||
| @@ -54,7 +60,7 @@ class AgentTableSerializer(serializers.ModelSerializer): | |||||||
|         else: |         else: | ||||||
|             agent_tz = self.context["default_tz"] |             agent_tz = self.context["default_tz"] | ||||||
|  |  | ||||||
|         return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S") |         return obj.last_seen.astimezone(agent_tz).timestamp() | ||||||
|  |  | ||||||
|     def get_logged_username(self, obj) -> str: |     def get_logged_username(self, obj) -> str: | ||||||
|         if obj.logged_in_username == "None" and obj.status == "online": |         if obj.logged_in_username == "None" and obj.status == "online": | ||||||
|   | |||||||
| @@ -19,9 +19,11 @@ logger.configure(**settings.LOG_CONFIG) | |||||||
| def _check_agent_service(pk: int) -> None: | def _check_agent_service(pk: int) -> None: | ||||||
|     agent = Agent.objects.get(pk=pk) |     agent = Agent.objects.get(pk=pk) | ||||||
|     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) |     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) | ||||||
|  |     # if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service) | ||||||
|  |     # then tacticalagent service is hung. forcefully restart it | ||||||
|     if r == "pong": |     if r == "pong": | ||||||
|         logger.info( |         logger.info( | ||||||
|             f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery" |             f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery" | ||||||
|         ) |         ) | ||||||
|         data = {"func": "recover", "payload": {"mode": "tacagent"}} |         data = {"func": "recover", "payload": {"mode": "tacagent"}} | ||||||
|         asyncio.run(agent.nats_cmd(data, wait=False)) |         asyncio.run(agent.nats_cmd(data, wait=False)) | ||||||
| @@ -49,7 +51,7 @@ def check_in_task() -> None: | |||||||
|  |  | ||||||
| @app.task | @app.task | ||||||
| def monitor_agents_task() -> None: | def monitor_agents_task() -> None: | ||||||
|     q = Agent.objects.all() |     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"] |     agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"] | ||||||
|     for agent in agents: |     for agent in agents: | ||||||
|         _check_agent_service(agent) |         _check_agent_service(agent) | ||||||
| @@ -62,9 +64,18 @@ def agent_update(pk: int) -> str: | |||||||
|         logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") |         logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") | ||||||
|         return "noarch" |         return "noarch" | ||||||
|  |  | ||||||
|     version = settings.LATEST_AGENT_VER |     # removed sqlite in 1.4.0 to get rid of cgo dependency | ||||||
|     url = agent.winagent_dl |     # 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent | ||||||
|     inno = agent.win_inno_exe |     if pyver.parse(agent.version) >= pyver.parse("1.3.0"): | ||||||
|  |         version = settings.LATEST_AGENT_VER | ||||||
|  |         url = agent.winagent_dl | ||||||
|  |         inno = agent.win_inno_exe | ||||||
|  |     else: | ||||||
|  |         version = "1.3.0" | ||||||
|  |         inno = ( | ||||||
|  |             "winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe" | ||||||
|  |         ) | ||||||
|  |         url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}" | ||||||
|  |  | ||||||
|     if agent.has_nats: |     if agent.has_nats: | ||||||
|         if pyver.parse(agent.version) <= pyver.parse("1.1.11"): |         if pyver.parse(agent.version) <= pyver.parse("1.1.11"): | ||||||
| @@ -100,6 +111,10 @@ def agent_update(pk: int) -> str: | |||||||
|             asyncio.run(agent.nats_cmd(nats_data, wait=False)) |             asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||||
|  |  | ||||||
|         return "created" |         return "created" | ||||||
|  |     else: | ||||||
|  |         logger.warning( | ||||||
|  |             f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to update." | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     return "not supported" |     return "not supported" | ||||||
|  |  | ||||||
| @@ -141,7 +156,7 @@ def auto_self_agent_update_task() -> None: | |||||||
|  |  | ||||||
| @app.task | @app.task | ||||||
| def get_wmi_task(): | def get_wmi_task(): | ||||||
|     agents = Agent.objects.all() |     agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time") | ||||||
|     online = [ |     online = [ | ||||||
|         i |         i | ||||||
|         for i in agents |         for i in agents | ||||||
| @@ -158,7 +173,7 @@ def get_wmi_task(): | |||||||
|  |  | ||||||
| @app.task | @app.task | ||||||
| def sync_sysinfo_task(): | def sync_sysinfo_task(): | ||||||
|     agents = Agent.objects.all() |     agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time") | ||||||
|     online = [ |     online = [ | ||||||
|         i |         i | ||||||
|         for i in agents |         for i in agents | ||||||
| @@ -307,7 +322,7 @@ def remove_salt_task() -> None: | |||||||
|     if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT: |     if hasattr(settings, "KEEP_SALT") and settings.KEEP_SALT: | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     q = Agent.objects.all() |     q = Agent.objects.only("pk", "version") | ||||||
|     agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] |     agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] | ||||||
|     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) |     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) | ||||||
|     for chunk in chunks: |     for chunk in chunks: | ||||||
|   | |||||||
| @@ -479,42 +479,20 @@ class TestAgentViews(TacticalTestCase): | |||||||
|     def test_overdue_action(self): |     def test_overdue_action(self): | ||||||
|         url = "/agents/overdueaction/" |         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") |         r = self.client.post(url, payload, format="json") | ||||||
|         self.assertEqual(r.status_code, 200) |         self.assertEqual(r.status_code, 200) | ||||||
|         agent = Agent.objects.get(pk=self.agent.pk) |         agent = Agent.objects.get(pk=self.agent.pk) | ||||||
|         self.assertTrue(agent.overdue_email_alert) |         self.assertTrue(agent.overdue_email_alert) | ||||||
|         self.assertEqual(self.agent.hostname, r.data) |         self.assertEqual(self.agent.hostname, r.data) | ||||||
|  |  | ||||||
|         payload.update({"alertType": "email", "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_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"}) |  | ||||||
|         r = self.client.post(url, payload, format="json") |         r = self.client.post(url, payload, format="json") | ||||||
|         self.assertEqual(r.status_code, 200) |         self.assertEqual(r.status_code, 200) | ||||||
|         agent = Agent.objects.get(pk=self.agent.pk) |         agent = Agent.objects.get(pk=self.agent.pk) | ||||||
|         self.assertFalse(agent.overdue_text_alert) |         self.assertFalse(agent.overdue_text_alert) | ||||||
|         self.assertEqual(self.agent.hostname, r.data) |         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) |         self.check_not_authenticated("post", url) | ||||||
|  |  | ||||||
|     def test_list_agents_no_detail(self): |     def test_list_agents_no_detail(self): | ||||||
| @@ -780,19 +758,20 @@ class TestAgentTasks(TacticalTestCase): | |||||||
|         action = PendingAction.objects.get(agent__pk=agent64_111.pk) |         action = PendingAction.objects.get(agent__pk=agent64_111.pk) | ||||||
|         self.assertEqual(action.action_type, "agentupdate") |         self.assertEqual(action.action_type, "agentupdate") | ||||||
|         self.assertEqual(action.status, "pending") |         self.assertEqual(action.status, "pending") | ||||||
|         self.assertEqual(action.details["url"], settings.DL_64) |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe" |             action.details["url"], | ||||||
|  |             "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER) |         self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe") | ||||||
|  |         self.assertEqual(action.details["version"], "1.3.0") | ||||||
|  |  | ||||||
|         agent64 = baker.make_recipe( |         agent_64_130 = baker.make_recipe( | ||||||
|             "agents.agent", |             "agents.agent", | ||||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", |             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||||
|             version="1.1.12", |             version="1.3.0", | ||||||
|         ) |         ) | ||||||
|         nats_cmd.return_value = "ok" |         nats_cmd.return_value = "ok" | ||||||
|         r = agent_update(agent64.pk) |         r = agent_update(agent_64_130.pk) | ||||||
|         self.assertEqual(r, "created") |         self.assertEqual(r, "created") | ||||||
|         nats_cmd.assert_called_with( |         nats_cmd.assert_called_with( | ||||||
|             { |             { | ||||||
| @@ -806,6 +785,26 @@ class TestAgentTasks(TacticalTestCase): | |||||||
|             wait=False, |             wait=False, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         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.models.Agent.salt_api_async") | ||||||
|     @patch("agents.tasks.sleep", return_value=None) |     @patch("agents.tasks.sleep", return_value=None) | ||||||
|     def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): |     def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ from .serializers import ( | |||||||
|     AgentEditSerializer, |     AgentEditSerializer, | ||||||
|     NoteSerializer, |     NoteSerializer, | ||||||
|     NotesSerializer, |     NotesSerializer, | ||||||
|  |     AgentOverdueActionSerializer, | ||||||
| ) | ) | ||||||
| from winupdate.serializers import WinUpdatePolicySerializer | from winupdate.serializers import WinUpdatePolicySerializer | ||||||
|  |  | ||||||
| @@ -333,26 +334,12 @@ def by_site(request, sitepk): | |||||||
|  |  | ||||||
| @api_view(["POST"]) | @api_view(["POST"]) | ||||||
| def overdue_action(request): | def overdue_action(request): | ||||||
|     pk = request.data["pk"] |     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||||
|     alert_type = request.data["alertType"] |     serializer = AgentOverdueActionSerializer( | ||||||
|     action = request.data["action"] |         instance=agent, data=request.data, partial=True | ||||||
|     agent = get_object_or_404(Agent, pk=pk) |     ) | ||||||
|     if alert_type == "email" and action == "enabled": |     serializer.is_valid(raise_exception=True) | ||||||
|         agent.overdue_email_alert = True |     serializer.save() | ||||||
|         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 |  | ||||||
|         ) |  | ||||||
|     return Response(agent.hostname) |     return Response(agent.hostname) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -473,7 +460,7 @@ def install_agent(request): | |||||||
|             f"GOARCH={goarch}", |             f"GOARCH={goarch}", | ||||||
|             go_bin, |             go_bin, | ||||||
|             "build", |             "build", | ||||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", |             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||||
|             f"-X 'main.Api={api}'", |             f"-X 'main.Api={api}'", | ||||||
|             f"-X 'main.Client={client_id}'", |             f"-X 'main.Client={client_id}'", | ||||||
|             f"-X 'main.Site={site_id}'", |             f"-X 'main.Site={site_id}'", | ||||||
| @@ -825,7 +812,7 @@ def bulk(request): | |||||||
|     elif request.data["target"] == "agents": |     elif request.data["target"] == "agents": | ||||||
|         q = Agent.objects.filter(pk__in=request.data["agentPKs"]) |         q = Agent.objects.filter(pk__in=request.data["agentPKs"]) | ||||||
|     elif request.data["target"] == "all": |     elif request.data["target"] == "all": | ||||||
|         q = Agent.objects.all() |         q = Agent.objects.only("pk", "monitoring_type") | ||||||
|     else: |     else: | ||||||
|         return notify_error("Something went wrong") |         return notify_error("Something went wrong") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -75,3 +75,12 @@ class TestAPIv3(TacticalTestCase): | |||||||
|         self.assertEqual(r.status_code, 200) |         self.assertEqual(r.status_code, 200) | ||||||
|  |  | ||||||
|         self.check_not_authenticated("patch", url) |         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") | ||||||
|  |         self.assertEqual(r.status_code, 200) | ||||||
|  |         self.assertEqual( | ||||||
|  |             r.json(), | ||||||
|  |             {"agent": self.agent.pk, "check_interval": self.agent.check_interval}, | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ urlpatterns = [ | |||||||
|     path("hello/", views.Hello.as_view()), |     path("hello/", views.Hello.as_view()), | ||||||
|     path("checkrunner/", views.CheckRunner.as_view()), |     path("checkrunner/", views.CheckRunner.as_view()), | ||||||
|     path("<str:agentid>/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>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()), | ||||||
|     path("<int:pk>/meshinfo/", views.MeshInfo.as_view()), |     path("<int:pk>/meshinfo/", views.MeshInfo.as_view()), | ||||||
|     path("meshexe/", views.MeshExe.as_view()), |     path("meshexe/", views.MeshExe.as_view()), | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ from autotasks.models import AutomatedTask | |||||||
| from accounts.models import User | from accounts.models import User | ||||||
| from winupdate.models import WinUpdatePolicy | from winupdate.models import WinUpdatePolicy | ||||||
| from software.models import InstalledSoftware | from software.models import InstalledSoftware | ||||||
| from checks.serializers import CheckRunnerGetSerializerV3 | from checks.serializers import CheckRunnerGetSerializer | ||||||
| from agents.serializers import WinAgentSerializer | from agents.serializers import WinAgentSerializer | ||||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||||
| from winupdate.serializers import ApprovedUpdateSerializer | from winupdate.serializers import ApprovedUpdateSerializer | ||||||
| @@ -232,7 +232,7 @@ class CheckRunner(APIView): | |||||||
|         ret = { |         ret = { | ||||||
|             "agent": agent.pk, |             "agent": agent.pk, | ||||||
|             "check_interval": agent.check_interval, |             "check_interval": agent.check_interval, | ||||||
|             "checks": CheckRunnerGetSerializerV3(checks, many=True).data, |             "checks": CheckRunnerGetSerializer(checks, many=True).data, | ||||||
|         } |         } | ||||||
|         return Response(ret) |         return Response(ret) | ||||||
|  |  | ||||||
| @@ -245,6 +245,15 @@ class CheckRunner(APIView): | |||||||
|         return Response(status) |         return Response(status) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CheckRunnerInterval(APIView): | ||||||
|  |     authentication_classes = [TokenAuthentication] | ||||||
|  |     permission_classes = [IsAuthenticated] | ||||||
|  |  | ||||||
|  |     def get(self, request, agentid): | ||||||
|  |         agent = get_object_or_404(Agent, agent_id=agentid) | ||||||
|  |         return Response({"agent": agent.pk, "check_interval": agent.check_interval}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TaskRunner(APIView): | class TaskRunner(APIView): | ||||||
|     """ |     """ | ||||||
|     For the windows golang agent |     For the windows golang agent | ||||||
| @@ -323,6 +332,7 @@ class WinUpdater(APIView): | |||||||
|             update.installed = True |             update.installed = True | ||||||
|             update.save(update_fields=["result", "downloaded", "installed"]) |             update.save(update_fields=["result", "downloaded", "installed"]) | ||||||
|  |  | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         return Response("ok") |         return Response("ok") | ||||||
|  |  | ||||||
|     # agent calls this after it's finished installing all patches |     # agent calls this after it's finished installing all patches | ||||||
| @@ -348,6 +358,7 @@ class WinUpdater(APIView): | |||||||
|                     f"{agent.hostname} is rebooting after updates were installed." |                     f"{agent.hostname} is rebooting after updates were installed." | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         return Response("ok") |         return Response("ok") | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,11 +11,15 @@ def generate_agent_checks_from_policies_task(policypk, create_tasks=False): | |||||||
|     policy = Policy.objects.get(pk=policypk) |     policy = Policy.objects.get(pk=policypk) | ||||||
|  |  | ||||||
|     if policy.is_default_server_policy and policy.is_default_workstation_policy: |     if policy.is_default_server_policy and policy.is_default_workstation_policy: | ||||||
|         agents = Agent.objects.all() |         agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") | ||||||
|     elif policy.is_default_server_policy: |     elif policy.is_default_server_policy: | ||||||
|         agents = Agent.objects.filter(monitoring_type="server") |         agents = Agent.objects.filter(monitoring_type="server").only( | ||||||
|  |             "pk", "monitoring_type" | ||||||
|  |         ) | ||||||
|     elif policy.is_default_workstation_policy: |     elif policy.is_default_workstation_policy: | ||||||
|         agents = Agent.objects.filter(monitoring_type="workstation") |         agents = Agent.objects.filter(monitoring_type="workstation").only( | ||||||
|  |             "pk", "monitoring_type" | ||||||
|  |         ) | ||||||
|     else: |     else: | ||||||
|         agents = policy.related_agents() |         agents = policy.related_agents() | ||||||
|  |  | ||||||
| @@ -84,11 +88,15 @@ def generate_agent_tasks_from_policies_task(policypk): | |||||||
|     policy = Policy.objects.get(pk=policypk) |     policy = Policy.objects.get(pk=policypk) | ||||||
|  |  | ||||||
|     if policy.is_default_server_policy and policy.is_default_workstation_policy: |     if policy.is_default_server_policy and policy.is_default_workstation_policy: | ||||||
|         agents = Agent.objects.all() |         agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") | ||||||
|     elif policy.is_default_server_policy: |     elif policy.is_default_server_policy: | ||||||
|         agents = Agent.objects.filter(monitoring_type="server") |         agents = Agent.objects.filter(monitoring_type="server").only( | ||||||
|  |             "pk", "monitoring_type" | ||||||
|  |         ) | ||||||
|     elif policy.is_default_workstation_policy: |     elif policy.is_default_workstation_policy: | ||||||
|         agents = Agent.objects.filter(monitoring_type="workstation") |         agents = Agent.objects.filter(monitoring_type="workstation").only( | ||||||
|  |             "pk", "monitoring_type" | ||||||
|  |         ) | ||||||
|     else: |     else: | ||||||
|         agents = policy.related_agents() |         agents = policy.related_agents() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -413,11 +413,15 @@ class UpdatePatchPolicy(APIView): | |||||||
|  |  | ||||||
|         agents = None |         agents = None | ||||||
|         if "client" in request.data: |         if "client" in request.data: | ||||||
|             agents = Agent.objects.filter(site__client_id=request.data["client"]) |             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||||
|  |                 site__client_id=request.data["client"] | ||||||
|  |             ) | ||||||
|         elif "site" in request.data: |         elif "site" in request.data: | ||||||
|             agents = Agent.objects.filter(site_id=request.data["site"]) |             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||||
|  |                 site_id=request.data["site"] | ||||||
|  |             ) | ||||||
|         else: |         else: | ||||||
|             agents = Agent.objects.all() |             agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk") | ||||||
|  |  | ||||||
|         for agent in agents: |         for agent in agents: | ||||||
|             winupdatepolicy = agent.winupdatepolicy.get() |             winupdatepolicy = agent.winupdatepolicy.get() | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ class Command(BaseCommand): | |||||||
|     help = "Checks for orphaned tasks on all agents and removes them" |     help = "Checks for orphaned tasks on all agents and removes them" | ||||||
|  |  | ||||||
|     def handle(self, *args, **kwargs): |     def handle(self, *args, **kwargs): | ||||||
|         agents = Agent.objects.all() |         agents = Agent.objects.only("pk", "last_seen", "overdue_time") | ||||||
|         online = [i for i in agents if i.status == "online"] |         online = [i for i in agents if i.status == "online"] | ||||||
|         for agent in online: |         for agent in online: | ||||||
|             remove_orphaned_win_tasks.delay(agent.pk) |             remove_orphaned_win_tasks.delay(agent.pk) | ||||||
|   | |||||||
| @@ -445,42 +445,6 @@ class Check(BaseAuditModel): | |||||||
|  |  | ||||||
|         return self.status |         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 |     @staticmethod | ||||||
|     def serialize(check): |     def serialize(check): | ||||||
|         # serializes the check and returns json |         # serializes the check and returns json | ||||||
|   | |||||||
| @@ -95,101 +95,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer): | |||||||
|  |  | ||||||
|  |  | ||||||
| class CheckRunnerGetSerializer(serializers.ModelSerializer): | class CheckRunnerGetSerializer(serializers.ModelSerializer): | ||||||
|     # for the windows agent |  | ||||||
|     # only send data needed for agent to run a check |     # 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() |     assigned_tasks = serializers.SerializerMethodField() | ||||||
|     script = ScriptCheckSerializer(read_only=True) |     script = ScriptCheckSerializer(read_only=True) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ from checks.models import CheckHistory | |||||||
| from tacticalrmm.test import TacticalTestCase | from tacticalrmm.test import TacticalTestCase | ||||||
| from .serializers import CheckSerializer | from .serializers import CheckSerializer | ||||||
| from django.utils import timezone as djangotime | from django.utils import timezone as djangotime | ||||||
|  | from unittest.mock import patch | ||||||
|  |  | ||||||
| from model_bakery import baker | from model_bakery import baker | ||||||
| from itertools import cycle |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCheckViews(TacticalTestCase): | class TestCheckViews(TacticalTestCase): | ||||||
| @@ -184,6 +184,48 @@ class TestCheckViews(TacticalTestCase): | |||||||
|  |  | ||||||
|         self.check_not_authenticated("patch", url_a) |         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): |     def test_get_check_history(self): | ||||||
|         # setup data |         # setup data | ||||||
|         agent = baker.make_recipe("agents.agent") |         agent = baker.make_recipe("agents.agent") | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | from packaging import version as pyver | ||||||
|  |  | ||||||
| from django.shortcuts import get_object_or_404 | from django.shortcuts import get_object_or_404 | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| @@ -168,8 +169,17 @@ def run_checks(request, pk): | |||||||
|     if not agent.has_nats: |     if not agent.has_nats: | ||||||
|         return notify_error("Requires agent version 1.1.0 or greater") |         return notify_error("Requires agent version 1.1.0 or greater") | ||||||
|  |  | ||||||
|     asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) |     if pyver.parse(agent.version) >= pyver.parse("1.4.1"): | ||||||
|     return Response(agent.hostname) |         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() | @api_view() | ||||||
|   | |||||||
| @@ -223,7 +223,7 @@ class GenerateAgent(APIView): | |||||||
|             f"GOARCH={goarch}", |             f"GOARCH={goarch}", | ||||||
|             go_bin, |             go_bin, | ||||||
|             "build", |             "build", | ||||||
|             f"-ldflags=\"-X 'main.Inno={inno}'", |             f"-ldflags=\"-s -w -X 'main.Inno={inno}'", | ||||||
|             f"-X 'main.Api={api}'", |             f"-X 'main.Api={api}'", | ||||||
|             f"-X 'main.Client={d.client.pk}'", |             f"-X 'main.Client={d.client.pk}'", | ||||||
|             f"-X 'main.Site={d.site.pk}'", |             f"-X 'main.Site={d.site.pk}'", | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ class Command(BaseCommand): | |||||||
|         # 10-16-2020 changed the type of the agent's 'disks' model field |         # 10-16-2020 changed the type of the agent's 'disks' model field | ||||||
|         # from a dict of dicts, to a list of disks in the golang agent |         # from a dict of dicts, to a list of disks in the golang agent | ||||||
|         # the following will convert dicts to lists for agent's still on the python agent |         # the following will convert dicts to lists for agent's still on the python agent | ||||||
|         agents = Agent.objects.all() |         agents = Agent.objects.only("pk", "disks") | ||||||
|         for agent in agents: |         for agent in agents: | ||||||
|             if agent.disks is not None and isinstance(agent.disks, dict): |             if agent.disks is not None and isinstance(agent.disks, dict): | ||||||
|                 new = [] |                 new = [] | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ def server_maintenance(request): | |||||||
|         from agents.models import Agent |         from agents.models import Agent | ||||||
|         from autotasks.tasks import remove_orphaned_win_tasks |         from autotasks.tasks import remove_orphaned_win_tasks | ||||||
|  |  | ||||||
|         agents = Agent.objects.all() |         agents = Agent.objects.only("pk", "last_seen", "overdue_time") | ||||||
|         online = [i for i in agents if i.status == "online"] |         online = [i for i in agents if i.status == "online"] | ||||||
|         for agent in online: |         for agent in online: | ||||||
|             remove_orphaned_win_tasks.delay(agent.pk) |             remove_orphaned_win_tasks.delay(agent.pk) | ||||||
|   | |||||||
| @@ -140,7 +140,7 @@ def cancel_pending_action(request): | |||||||
| def debug_log(request, mode, hostname, order): | def debug_log(request, mode, hostname, order): | ||||||
|     log_file = settings.LOG_CONFIG["handlers"][0]["sink"] |     log_file = settings.LOG_CONFIG["handlers"][0]["sink"] | ||||||
|  |  | ||||||
|     agents = Agent.objects.all() |     agents = Agent.objects.prefetch_related("site").only("pk", "hostname") | ||||||
|     agent_hostnames = AgentHostnameSerializer(agents, many=True) |     agent_hostnames = AgentHostnameSerializer(agents, many=True) | ||||||
|  |  | ||||||
|     switch_mode = { |     switch_mode = { | ||||||
|   | |||||||
| @@ -176,6 +176,7 @@ class NatsWinUpdates(APIView): | |||||||
|             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) |             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||||
|             logger.info(f"{agent.hostname} is rebooting after updates were installed.") |             logger.info(f"{agent.hostname} is rebooting after updates were installed.") | ||||||
|  |  | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         return Response("ok") |         return Response("ok") | ||||||
|  |  | ||||||
|     def patch(self, request): |     def patch(self, request): | ||||||
| @@ -199,6 +200,7 @@ class NatsWinUpdates(APIView): | |||||||
|             u.result = "failed" |             u.result = "failed" | ||||||
|             u.save(update_fields=["result"]) |             u.save(update_fields=["result"]) | ||||||
|  |  | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         return Response("ok") |         return Response("ok") | ||||||
|  |  | ||||||
|     def post(self, request): |     def post(self, request): | ||||||
| @@ -233,4 +235,5 @@ class NatsWinUpdates(APIView): | |||||||
|                     revision_number=update["revision_number"], |                     revision_number=update["revision_number"], | ||||||
|                 ).save() |                 ).save() | ||||||
|  |  | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         return Response("ok") |         return Response("ok") | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
| black | black | ||||||
| Werkzeug | Werkzeug | ||||||
| django-extensions | django-extensions | ||||||
|  | mkdocs | ||||||
|  | mkdocs-material | ||||||
|  | pymdown-extensions | ||||||
| @@ -193,6 +193,6 @@ | |||||||
|         "submittedBy": "https://github.com/dinger1986", |         "submittedBy": "https://github.com/dinger1986", | ||||||
|         "name": "TRMM Defender Exclusions", |         "name": "TRMM Defender Exclusions", | ||||||
|         "description": "Windows Defender Exclusions for Tactical RMM", |         "description": "Windows Defender Exclusions for Tactical RMM", | ||||||
|         "shell": "cmd" |         "shell": "powershell" | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ class Script(BaseAuditModel): | |||||||
|                     i.name = script["name"] |                     i.name = script["name"] | ||||||
|                     i.description = script["description"] |                     i.description = script["description"] | ||||||
|                     i.category = "Community" |                     i.category = "Community" | ||||||
|  |                     i.shell = script["shell"] | ||||||
|  |  | ||||||
|                     with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: |                     with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: | ||||||
|                         script_bytes = ( |                         script_bytes = ( | ||||||
| @@ -80,7 +81,13 @@ class Script(BaseAuditModel): | |||||||
|                         i.code_base64 = base64.b64encode(script_bytes).decode("ascii") |                         i.code_base64 = base64.b64encode(script_bytes).decode("ascii") | ||||||
|  |  | ||||||
|                     i.save( |                     i.save( | ||||||
|                         update_fields=["name", "description", "category", "code_base64"] |                         update_fields=[ | ||||||
|  |                             "name", | ||||||
|  |                             "description", | ||||||
|  |                             "category", | ||||||
|  |                             "code_base64", | ||||||
|  |                             "shell", | ||||||
|  |                         ] | ||||||
|                     ) |                     ) | ||||||
|                 else: |                 else: | ||||||
|                     print(f"Adding new community script: {script['name']}") |                     print(f"Adding new community script: {script['name']}") | ||||||
|   | |||||||
| @@ -15,16 +15,16 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe") | |||||||
| AUTH_USER_MODEL = "accounts.User" | AUTH_USER_MODEL = "accounts.User" | ||||||
|  |  | ||||||
| # latest release | # latest release | ||||||
| TRMM_VERSION = "0.4.0" | TRMM_VERSION = "0.4.2" | ||||||
|  |  | ||||||
| # bump this version everytime vue code is changed | # bump this version everytime vue code is changed | ||||||
| # to alert user they need to manually refresh their browser | # to alert user they need to manually refresh their browser | ||||||
| APP_VER = "0.0.107" | APP_VER = "0.0.109" | ||||||
|  |  | ||||||
| # https://github.com/wh1te909/rmmagent | # https://github.com/wh1te909/rmmagent | ||||||
| LATEST_AGENT_VER = "1.3.0" | LATEST_AGENT_VER = "1.4.1" | ||||||
|  |  | ||||||
| MESH_VER = "0.7.49" | MESH_VER = "0.7.54" | ||||||
|  |  | ||||||
| # for the update script, bump when need to recreate venv or npm install | # for the update script, bump when need to recreate venv or npm install | ||||||
| PIP_VER = "7" | PIP_VER = "7" | ||||||
|   | |||||||
| @@ -19,8 +19,9 @@ logger.configure(**settings.LOG_CONFIG) | |||||||
| def auto_approve_updates_task(): | def auto_approve_updates_task(): | ||||||
|     # scheduled task that checks and approves updates daily |     # scheduled task that checks and approves updates daily | ||||||
|  |  | ||||||
|     agents = Agent.objects.all() |     agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time") | ||||||
|     for agent in agents: |     for agent in agents: | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         try: |         try: | ||||||
|             agent.approve_updates() |             agent.approve_updates() | ||||||
|         except: |         except: | ||||||
| @@ -43,7 +44,7 @@ def auto_approve_updates_task(): | |||||||
| @app.task | @app.task | ||||||
| def check_agent_update_schedule_task(): | def check_agent_update_schedule_task(): | ||||||
|     # scheduled task that installs updates on agents if enabled |     # scheduled task that installs updates on agents if enabled | ||||||
|     agents = Agent.objects.all() |     agents = Agent.objects.only("pk", "version", "last_seen", "overdue_time") | ||||||
|     online = [ |     online = [ | ||||||
|         i |         i | ||||||
|         for i in agents |         for i in agents | ||||||
| @@ -53,6 +54,7 @@ def check_agent_update_schedule_task(): | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     for agent in online: |     for agent in online: | ||||||
|  |         agent.delete_superseded_updates() | ||||||
|         install = False |         install = False | ||||||
|         patch_policy = agent.get_patch_policy() |         patch_policy = agent.get_patch_policy() | ||||||
|  |  | ||||||
| @@ -126,6 +128,7 @@ def bulk_install_updates_task(pks: List[int]) -> None: | |||||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) |     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||||
|     for chunk in chunks: |     for chunk in chunks: | ||||||
|         for agent in chunk: |         for agent in chunk: | ||||||
|  |             agent.delete_superseded_updates() | ||||||
|             nats_data = { |             nats_data = { | ||||||
|                 "func": "installwinupdates", |                 "func": "installwinupdates", | ||||||
|                 "guids": agent.get_approved_update_guids(), |                 "guids": agent.get_approved_update_guids(), | ||||||
| @@ -142,6 +145,7 @@ def bulk_check_for_updates_task(pks: List[int]) -> None: | |||||||
|     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) |     chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) | ||||||
|     for chunk in chunks: |     for chunk in chunks: | ||||||
|         for agent in chunk: |         for agent in chunk: | ||||||
|  |             agent.delete_superseded_updates() | ||||||
|             asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) |             asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) | ||||||
|             time.sleep(0.05) |             time.sleep(0.05) | ||||||
|         time.sleep(15) |         time.sleep(15) | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ def get_win_updates(request, pk): | |||||||
| @api_view() | @api_view() | ||||||
| def run_update_scan(request, pk): | def run_update_scan(request, pk): | ||||||
|     agent = get_object_or_404(Agent, pk=pk) |     agent = get_object_or_404(Agent, pk=pk) | ||||||
|  |     agent.delete_superseded_updates() | ||||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): |     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||||
|         return notify_error("Requires agent version 1.3.0 or greater") |         return notify_error("Requires agent version 1.3.0 or greater") | ||||||
|  |  | ||||||
| @@ -32,6 +33,7 @@ def run_update_scan(request, pk): | |||||||
| @api_view() | @api_view() | ||||||
| def install_updates(request, pk): | def install_updates(request, pk): | ||||||
|     agent = get_object_or_404(Agent, pk=pk) |     agent = get_object_or_404(Agent, pk=pk) | ||||||
|  |     agent.delete_superseded_updates() | ||||||
|     if pyver.parse(agent.version) < pyver.parse("1.3.0"): |     if pyver.parse(agent.version) < pyver.parse("1.3.0"): | ||||||
|         return notify_error("Requires agent version 1.3.0 or greater") |         return notify_error("Requires agent version 1.3.0 or greater") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +0,0 @@ | |||||||
| pids |  | ||||||
| logs |  | ||||||
| node_modules |  | ||||||
| npm-debug.log |  | ||||||
| coverage/ |  | ||||||
| run |  | ||||||
| dist |  | ||||||
| .DS_Store |  | ||||||
| .nyc_output |  | ||||||
| .basement |  | ||||||
| config.local.js |  | ||||||
| basement_dist |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| const { description } = require('../package') |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   base: '/tacticalrmm/', |  | ||||||
|   title: 'Tactical RMM', |  | ||||||
|   description: description, |  | ||||||
|  |  | ||||||
|   head: [ |  | ||||||
|     ['meta', { name: 'theme-color', content: '#3eaf7c' }], |  | ||||||
|     ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], |  | ||||||
|     ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }] |  | ||||||
|   ], |  | ||||||
|   themeConfig: { |  | ||||||
|     repo: '', |  | ||||||
|     editLinks: false, |  | ||||||
|     docsDir: '', |  | ||||||
|     editLinkText: '', |  | ||||||
|     lastUpdated: false, |  | ||||||
|     nav: [ |  | ||||||
|       { |  | ||||||
|         text: 'Guide', |  | ||||||
|         link: '/guide/', |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     sidebar: { |  | ||||||
|       '/guide/': [ |  | ||||||
|         { |  | ||||||
|           title: 'Guide', |  | ||||||
|           collapsable: false, |  | ||||||
|           children: [ |  | ||||||
|             '', |  | ||||||
|           ] |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   plugins: [ |  | ||||||
|     //'@vuepress/plugin-back-to-top', |  | ||||||
|     //'@vuepress/plugin-medium-zoom', |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Client app enhancement file. |  | ||||||
|  * |  | ||||||
|  * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| export default ({ |  | ||||||
|   Vue, // the version of Vue being used in the VuePress app |  | ||||||
|   options, // the options for the root Vue instance |  | ||||||
|   router, // the router instance for the app |  | ||||||
|   siteData // site metadata |  | ||||||
| }) => { |  | ||||||
|   // ...apply enhancements for the site. |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Custom Styles here. |  | ||||||
|  * |  | ||||||
|  * ref:https://v1.vuepress.vuejs.org/config/#index-styl |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| .home .hero img |  | ||||||
|   max-width 450px!important |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Custom palette here. |  | ||||||
|  * |  | ||||||
|  * ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| $accentColor = #3eaf7c |  | ||||||
| $textColor = #2c3e50 |  | ||||||
| $borderColor = #eaecef |  | ||||||
| $codeBgColor = #282c34 |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 758 B | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/onit.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/onit.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 48 KiB | 
							
								
								
									
										28
									
								
								docs/docs/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								docs/docs/index.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | # Tactical RMM Documentation | ||||||
|  |  | ||||||
|  | [](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop) | ||||||
|  | [](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop) | ||||||
|  | [](https://opensource.org/licenses/MIT) | ||||||
|  | [](https://github.com/python/black) | ||||||
|  |  | ||||||
|  | Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django, Vue and Golang. | ||||||
|  | It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) | ||||||
|  |  | ||||||
|  | ## [LIVE DEMO](https://rmm.xlawgaming.com/) | ||||||
|  | Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app. | ||||||
|  |  | ||||||
|  | *Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.* | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - Teamviewer-like remote desktop control | ||||||
|  | - Real-time remote shell | ||||||
|  | - Remote file browser (download and upload files) | ||||||
|  | - Remote command and script execution (batch, powershell and python scripts) | ||||||
|  | - Event log viewer | ||||||
|  | - Services management | ||||||
|  | - Windows patch management | ||||||
|  | - Automated checks with email/SMS alerting (cpu, disk, memory, services, scripts, event logs) | ||||||
|  | - Automated task runner (run scripts on a schedule) | ||||||
|  | - Remote software installation via chocolatey | ||||||
|  | - Software and hardware inventory | ||||||
							
								
								
									
										10
									
								
								docs/docs/stylesheets/extra.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docs/docs/stylesheets/extra.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | .md-header { | ||||||
|  |     background-color: black !important; | ||||||
|  |     color: white !important; | ||||||
|  | } | ||||||
|  | .md-search__input { | ||||||
|  |     background-color: white !important; | ||||||
|  | } | ||||||
|  | .md-search__icon[for=__search]{ | ||||||
|  |     color: initial; | ||||||
|  | } | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| # Installation |  | ||||||
|  |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| --- |  | ||||||
| home: true |  | ||||||
| heroImage: https://v1.vuepress.vuejs.org/hero.png |  | ||||||
| actionText: Documentation → |  | ||||||
| actionLink: /guide/ |  | ||||||
| --- |  | ||||||
							
								
								
									
										33
									
								
								docs/mkdocs.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								docs/mkdocs.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | site_name: "Tactical RMM" | ||||||
|  | nav: | ||||||
|  |   - Home: index.md | ||||||
|  | site_description: "A remote monitoring and management tool for Windows computers" | ||||||
|  | site_author: "wh1te909" | ||||||
|  |  | ||||||
|  | # Repository | ||||||
|  | repo_name: "wh1te909/tacticalrmm" | ||||||
|  | repo_url: "https://github.com/wh1te909/tacticalrmm" | ||||||
|  | edit_uri: "" | ||||||
|  |  | ||||||
|  | theme: | ||||||
|  |   name: "material" | ||||||
|  |   custom_dir: "theme" | ||||||
|  |   logo: "images/onit.ico" | ||||||
|  |   favicon: "images/favicon.ico" | ||||||
|  |   language: "en" | ||||||
|  |   palette: | ||||||
|  |     primary: "white" | ||||||
|  |     accent: "indigo" | ||||||
|  | extra_css: | ||||||
|  |   - stylesheets/extra.css | ||||||
|  | extra: | ||||||
|  |   social: | ||||||
|  |     - icon: fontawesome/brands/github | ||||||
|  |       link: "https://github.com/wh1te909/tacticalrmm" | ||||||
|  | markdown_extensions: | ||||||
|  |   - pymdownx.inlinehilite | ||||||
|  |   - admonition | ||||||
|  |   - codehilite: | ||||||
|  |       guess_lang: false | ||||||
|  |   - toc: | ||||||
|  |       permalink: true | ||||||
							
								
								
									
										10783
									
								
								docs/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10783
									
								
								docs/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,14 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "tacticalrmm", |  | ||||||
|   "description": "A remote monitoring and management tool", |  | ||||||
|   "private": true, |  | ||||||
|   "version": "0.0.1", |  | ||||||
|   "scripts": { |  | ||||||
|     "dev": "vuepress dev", |  | ||||||
|     "build": "vuepress build" |  | ||||||
|   }, |  | ||||||
|   "license": "MIT", |  | ||||||
|   "devDependencies": { |  | ||||||
|     "vuepress": "^1.5.3" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										4
									
								
								docs/theme/main.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								docs/theme/main.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% block site_nav %} | ||||||
|  | {{ super() }} | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										70
									
								
								docs/theme/partials/footer.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/theme/partials/footer.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | {% import "partials/language.html" as lang with context %} | ||||||
|  |  | ||||||
|  | <!-- Application footer --> | ||||||
|  | <footer class="md-footer"> | ||||||
|  |  | ||||||
|  |   <!-- Link to previous and/or next page --> | ||||||
|  |   {% if page.previous_page or page.next_page %} | ||||||
|  |   <div class="md-footer-nav"> | ||||||
|  |     <nav class="md-footer-nav__inner md-grid"> | ||||||
|  |  | ||||||
|  |       <!-- Link to previous page --> | ||||||
|  |       {% if page.previous_page %} | ||||||
|  |       <a href="{{ page.previous_page.url | url }}" title="{{ page.previous_page.title }}" | ||||||
|  |         class="md-flex md-footer-nav__link md-footer-nav__link--prev" rel="prev"> | ||||||
|  |         <div class="md-flex__cell md-flex__cell--shrink"> | ||||||
|  |           <i class="md-icon md-icon--arrow-back | ||||||
|  |                     md-footer-nav__button"></i> | ||||||
|  |         </div> | ||||||
|  |         <div class="md-flex__cell md-flex__cell--stretch | ||||||
|  |                   md-footer-nav__title"> | ||||||
|  |           <span class="md-flex__ellipsis"> | ||||||
|  |             <span class="md-footer-nav__direction"> | ||||||
|  |               {{ lang.t("footer.previous") }} | ||||||
|  |             </span> | ||||||
|  |             {{ page.previous_page.title }} | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |       </a> | ||||||
|  |       {% endif %} | ||||||
|  |  | ||||||
|  |       <!-- Link to next page --> | ||||||
|  |       {% if page.next_page %} | ||||||
|  |       <a href="{{ page.next_page.url | url }}" title="{{ page.next_page.title }}" | ||||||
|  |         class="md-flex md-footer-nav__link md-footer-nav__link--next" rel="next"> | ||||||
|  |         <div class="md-flex__cell md-flex__cell--stretch | ||||||
|  |                   md-footer-nav__title"> | ||||||
|  |           <span class="md-flex__ellipsis"> | ||||||
|  |             <span class="md-footer-nav__direction"> | ||||||
|  |               {{ lang.t("footer.next") }} | ||||||
|  |             </span> | ||||||
|  |             {{ page.next_page.title }} | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |         <div class="md-flex__cell md-flex__cell--shrink"> | ||||||
|  |           <i class="md-icon md-icon--arrow-forward | ||||||
|  |                     md-footer-nav__button"></i> | ||||||
|  |         </div> | ||||||
|  |       </a> | ||||||
|  |       {% endif %} | ||||||
|  |     </nav> | ||||||
|  |   </div> | ||||||
|  |   {% endif %} | ||||||
|  |  | ||||||
|  |   <!-- Further information --> | ||||||
|  |   <div class="md-footer-meta md-typeset"> | ||||||
|  |     <div class="md-footer-meta__inner md-grid"> | ||||||
|  |  | ||||||
|  |       <!-- Copyright and theme information --> | ||||||
|  |       <div class="md-footer-copyright"> | ||||||
|  |         {% if config.copyright %} | ||||||
|  |         <div class="md-footer-copyright__highlight"> | ||||||
|  |           {{ config.copyright }} | ||||||
|  |         </div> | ||||||
|  |         {% endif %} | ||||||
|  |       </div> | ||||||
|  |       <!-- Social links --> | ||||||
|  |       {% include "partials/social.html" %} | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </footer> | ||||||
							
								
								
									
										4
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								main.go
									
									
									
									
									
								
							| @@ -9,7 +9,7 @@ import ( | |||||||
| 	"github.com/wh1te909/tacticalrmm/natsapi" | 	"github.com/wh1te909/tacticalrmm/natsapi" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var version = "1.0.1" | var version = "1.0.2" | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	ver := flag.Bool("version", false, "Prints version") | 	ver := flag.Bool("version", false, "Prints version") | ||||||
| @@ -23,5 +23,5 @@ func main() { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	api.Listen(*apiHost, *natsHost, *debug) | 	api.Listen(*apiHost, *natsHost, version, *debug) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,16 +40,15 @@ func getAPI(apihost, natshost string) (string, string, error) { | |||||||
| 	return "", "", errors.New("unable to parse api from nginx conf") | 	return "", "", errors.New("unable to parse api from nginx conf") | ||||||
| } | } | ||||||
|  |  | ||||||
| func Listen(apihost, natshost string, debug bool) { | func Listen(apihost, natshost, version string, debug bool) { | ||||||
| 	api, natsurl, err := getAPI(apihost, natshost) | 	api, natsurl, err := getAPI(apihost, natshost) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalln(err) | 		log.Fatalln(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if debug { | 	log.Printf("Tactical Nats API Version %s\n", version) | ||||||
| 		log.Println("Api base url: ", api) | 	log.Println("Api base url: ", api) | ||||||
| 		log.Println("Nats connection url: ", natsurl) | 	log.Println("Nats connection url: ", natsurl) | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rClient.SetHostURL(api) | 	rClient.SetHostURL(api) | ||||||
| 	rClient.SetTimeout(30 * time.Second) | 	rClient.SetTimeout(30 * time.Second) | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| @@ -229,8 +229,8 @@ export default { | |||||||
|             .then(response => { |             .then(response => { | ||||||
|               this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`)); |               this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`)); | ||||||
|             }) |             }) | ||||||
|             .catch(error => { |             .catch(e => { | ||||||
|               this.$q.notify(notifyErrorConfig(`An Error occured while deleting user ${data.username}`)); |               this.$q.notify(notifyErrorConfig(e.response.data)); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
| @@ -295,8 +295,8 @@ export default { | |||||||
|             .then(response => { |             .then(response => { | ||||||
|               this.$q.notify(notifySuccessConfig(response.data, 4000)); |               this.$q.notify(notifySuccessConfig(response.data, 4000)); | ||||||
|             }) |             }) | ||||||
|             .catch(error => { |             .catch(e => { | ||||||
|               this.$q.notify(notifyErrorConfig("An Error occured while resetting key")); |               this.$q.notify(notifyErrorConfig(e.response.data)); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -317,7 +317,7 @@ | |||||||
|               <q-tooltip>Reboot required</q-tooltip> |               <q-tooltip>Reboot required</q-tooltip> | ||||||
|             </q-icon> |             </q-icon> | ||||||
|           </q-td> |           </q-td> | ||||||
|           <q-td key="lastseen" :props="props">{{ formatDate(props.row.last_seen) }}</q-td> |           <q-td key="lastseen" :props="props">{{ unixToString(props.row.last_seen) }}</q-td> | ||||||
|           <q-td key="boottime" :props="props">{{ bootTime(props.row.boot_time) }}</q-td> |           <q-td key="boottime" :props="props">{{ bootTime(props.row.boot_time) }}</q-td> | ||||||
|         </q-tr> |         </q-tr> | ||||||
|       </template> |       </template> | ||||||
| @@ -363,7 +363,7 @@ import axios from "axios"; | |||||||
| import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins"; | import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins"; | ||||||
| import mixins from "@/mixins/mixins"; | import mixins from "@/mixins/mixins"; | ||||||
| import { mapGetters } from "vuex"; | import { mapGetters } from "vuex"; | ||||||
| import { openURL } from "quasar"; | import { date } from "quasar"; | ||||||
| import EditAgent from "@/components/modals/agents/EditAgent"; | import EditAgent from "@/components/modals/agents/EditAgent"; | ||||||
| import RebootLater from "@/components/modals/agents/RebootLater"; | import RebootLater from "@/components/modals/agents/RebootLater"; | ||||||
| import PendingActions from "@/components/modals/logs/PendingActions"; | import PendingActions from "@/components/modals/logs/PendingActions"; | ||||||
| @@ -440,9 +440,10 @@ export default { | |||||||
|           if (availability === "online" && row.status !== "online") return false; |           if (availability === "online" && row.status !== "online") return false; | ||||||
|           else if (availability === "offline" && row.status !== "overdue") return false; |           else if (availability === "offline" && row.status !== "overdue") return false; | ||||||
|           else if (availability === "expired") { |           else if (availability === "expired") { | ||||||
|             const nowPlus30Days = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); |             let now = new Date(); | ||||||
|             const unixtime = Date.parse(row.last_seen); |             let lastSeen = new Date(row.last_seen * 1000); | ||||||
|             if (unixtime > nowPlus30Days) return false; |             let diff = date.getDateDiff(now, lastSeen, "days"); | ||||||
|  |             if (diff < 30) return false; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -540,10 +541,17 @@ export default { | |||||||
|       window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826"); |       window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826"); | ||||||
|     }, |     }, | ||||||
|     runChecks(pk) { |     runChecks(pk) { | ||||||
|       axios |       this.$q.loading.show(); | ||||||
|  |       this.$axios | ||||||
|         .get(`/checks/runchecks/${pk}/`) |         .get(`/checks/runchecks/${pk}/`) | ||||||
|         .then(r => this.notifySuccess(`Checks will now be re-run on ${r.data}`)) |         .then(r => { | ||||||
|         .catch(e => this.notifyError(e.response.data)); |           this.$q.loading.hide(); | ||||||
|  |           this.notifySuccess(r.data); | ||||||
|  |         }) | ||||||
|  |         .catch(e => { | ||||||
|  |           this.$q.loading.hide(); | ||||||
|  |           this.notifyError(e.response.data); | ||||||
|  |         }); | ||||||
|     }, |     }, | ||||||
|     removeAgent(pk, name) { |     removeAgent(pk, name) { | ||||||
|       this.$q |       this.$q | ||||||
| @@ -636,14 +644,14 @@ export default { | |||||||
|       this.$store.dispatch("loadNotes", pk); |       this.$store.dispatch("loadNotes", pk); | ||||||
|     }, |     }, | ||||||
|     overdueAlert(category, pk, alert_action) { |     overdueAlert(category, pk, alert_action) { | ||||||
|  |       const db_field = category === "email" ? "overdue_email_alert" : "overdue_text_alert"; | ||||||
|       const action = alert_action ? "enabled" : "disabled"; |       const action = alert_action ? "enabled" : "disabled"; | ||||||
|       const data = { |       const data = { | ||||||
|         pk: pk, |         pk: pk, | ||||||
|         alertType: category, |         [db_field]: alert_action, | ||||||
|         action: action, |  | ||||||
|       }; |       }; | ||||||
|       const alertColor = alert_action ? "positive" : "warning"; |       const alertColor = alert_action ? "positive" : "warning"; | ||||||
|       axios |       this.$axios | ||||||
|         .post("/agents/overdueaction/", data) |         .post("/agents/overdueaction/", data) | ||||||
|         .then(r => { |         .then(r => { | ||||||
|           this.$q.notify({ |           this.$q.notify({ | ||||||
| @@ -652,7 +660,7 @@ export default { | |||||||
|             message: `Overdue ${category} alerts ${action} on ${r.data}`, |             message: `Overdue ${category} alerts ${action} on ${r.data}`, | ||||||
|           }); |           }); | ||||||
|         }) |         }) | ||||||
|         .catch(e => this.notifyError(e.response.data.error)); |         .catch(() => this.notifyError("Something went wrong")); | ||||||
|     }, |     }, | ||||||
|     agentClass(status) { |     agentClass(status) { | ||||||
|       if (status === "offline") { |       if (status === "offline") { | ||||||
|   | |||||||
| @@ -298,7 +298,6 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import axios from "axios"; |  | ||||||
| import mixins from "@/mixins/mixins"; | import mixins from "@/mixins/mixins"; | ||||||
| import { mapState } from "vuex"; | import { mapState } from "vuex"; | ||||||
| import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy"; | import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy"; | ||||||
| @@ -329,7 +328,7 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     getCoreSettings() { |     getCoreSettings() { | ||||||
|       axios.get("/core/getcoresettings/").then(r => { |       this.$axios.get("/core/getcoresettings/").then(r => { | ||||||
|         this.settings = r.data; |         this.settings = r.data; | ||||||
|         this.allTimezones = Object.freeze(r.data.all_timezones); |         this.allTimezones = Object.freeze(r.data.all_timezones); | ||||||
|         this.ready = true; |         this.ready = true; | ||||||
| @@ -388,7 +387,8 @@ export default { | |||||||
|     }, |     }, | ||||||
|     editSettings() { |     editSettings() { | ||||||
|       this.$q.loading.show(); |       this.$q.loading.show(); | ||||||
|       axios |       delete this.settings.all_timezones; | ||||||
|  |       this.$axios | ||||||
|         .patch("/core/editsettings/", this.settings) |         .patch("/core/editsettings/", this.settings) | ||||||
|         .then(r => { |         .then(r => { | ||||||
|           this.$q.loading.hide(); |           this.$q.loading.hide(); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Notify } from "quasar"; | import { Notify, date } from "quasar"; | ||||||
|  |  | ||||||
| export function notifySuccessConfig(msg, timeout = 2000) { | export function notifySuccessConfig(msg, timeout = 2000) { | ||||||
|   return { |   return { | ||||||
| @@ -95,6 +95,10 @@ export default { | |||||||
|  |  | ||||||
|       return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted |       return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted | ||||||
|     }, |     }, | ||||||
|  |     unixToString(timestamp) { | ||||||
|  |       let t = new Date(timestamp * 1000) | ||||||
|  |       return date.formatDate(t, 'MMM-D-YYYY - HH:mm') | ||||||
|  |     }, | ||||||
|     formatClientOptions(clients) { |     formatClientOptions(clients) { | ||||||
|       return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites })) |       return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites })) | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -661,7 +661,7 @@ export default { | |||||||
|       this.poll = setInterval(() => { |       this.poll = setInterval(() => { | ||||||
|         this.$store.dispatch("checkVer"); |         this.$store.dispatch("checkVer"); | ||||||
|         this.getAgentCounts(); |         this.getAgentCounts(); | ||||||
|         this.getDashInfo(); |         this.getDashInfo(false); | ||||||
|       }, 60 * 5 * 1000); |       }, 60 * 5 * 1000); | ||||||
|     }, |     }, | ||||||
|     setSplitter(val) { |     setSplitter(val) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user