From dbd1f0d4f9095cd88dd63d949d6fdb378999151d Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Mon, 1 Mar 2021 20:40:46 +0000 Subject: [PATCH] pending actions refactor --- api/tacticalrmm/logs/tests.py | 104 +++++++++++------- api/tacticalrmm/logs/urls.py | 4 +- api/tacticalrmm/logs/views.py | 52 +++++---- .../components/modals/logs/PendingActions.vue | 31 +++--- 4 files changed, 111 insertions(+), 80 deletions(-) diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index 9fcfea2b..ac854c83 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -3,10 +3,9 @@ from unittest.mock import patch from model_bakery import baker, seq +from logs.models import PendingAction from tacticalrmm.test import TacticalTestCase -from .serializers import PendingActionSerializer - class TestAuditViews(TacticalTestCase): def setUp(self): @@ -177,63 +176,94 @@ class TestAuditViews(TacticalTestCase): self.check_not_authenticated("post", url) - def test_agent_pending_actions(self): - agent = baker.make_recipe("agents.agent") - pending_actions = baker.make( + def test_get_pending_actions(self): + url = "/logs/pendingactions/" + agent1 = baker.make_recipe("agents.online_agent") + agent2 = baker.make_recipe("agents.online_agent") + + baker.make( "logs.PendingAction", - agent=agent, - _quantity=6, + agent=agent1, + action_type="chocoinstall", + details={"name": "googlechrome", "output": None, "installed": False}, + _quantity=12, + ) + baker.make( + "logs.PendingAction", + agent=agent2, + action_type="chocoinstall", + status="completed", + details={"name": "adobereader", "output": None, "installed": False}, + _quantity=14, ) - url = f"/logs/{agent.pk}/pendingactions/" - resp = self.client.get(url, format="json") - serializer = PendingActionSerializer(pending_actions, many=True) + data = {"showCompleted": False} + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data["actions"]), 12) # type: ignore + self.assertEqual(r.data["completed_count"], 14) # type: ignore - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 6) - self.assertEqual(resp.data, serializer.data) + PendingAction.objects.filter(action_type="chocoinstall").update( + status="completed" + ) + data = {"showCompleted": True} + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data["actions"]), 26) # type: ignore + self.assertEqual(r.data["completed_count"], 26) # type: ignore - self.check_not_authenticated("get", url) + data = {"showCompleted": True, "agentPK": agent1.pk} + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(len(r.data["actions"]), 12) # type: ignore + self.assertEqual(r.data["completed_count"], 26) # type: ignore - def test_all_pending_actions(self): - url = "/logs/allpendingactions/" - agent = baker.make_recipe("agents.agent") - pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6) - - resp = self.client.get(url, format="json") - serializer = PendingActionSerializer(pending_actions, many=True) - - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 6) - self.assertEqual(resp.data, serializer.data) - - self.check_not_authenticated("get", url) + self.check_not_authenticated("patch", url) @patch("agents.models.Agent.nats_cmd") def test_cancel_pending_action(self, nats_cmd): - url = "/logs/cancelpendingaction/" - # TODO fix this TypeError: Object of type coroutine is not JSON serializable - """ agent = baker.make("agents.Agent", version="1.1.1") - pending_action = baker.make( + nats_cmd.return_value = "ok" + url = "/logs/pendingactions/" + agent = baker.make_recipe("agents.online_agent") + action = baker.make( "logs.PendingAction", agent=agent, + action_type="schedreboot", details={ "time": "2021-01-13 18:20:00", "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", }, ) - data = {"pk": pending_action.id} - resp = self.client.delete(url, data, format="json") - self.assertEqual(resp.status_code, 200) + data = {"pk": action.pk} # type: ignore + r = self.client.delete(url, data, format="json") + self.assertEqual(r.status_code, 200) nats_data = { "func": "delschedtask", "schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"}, } nats_cmd.assert_called_with(nats_data, timeout=10) - # try request again and it should fail since pending action doesn't exist - resp = self.client.delete(url, data, format="json") - self.assertEqual(resp.status_code, 404) """ + # try request again and it should 404 since pending action doesn't exist + r = self.client.delete(url, data, format="json") + self.assertEqual(r.status_code, 404) + + nats_cmd.reset_mock() + + action2 = baker.make( + "logs.PendingAction", + agent=agent, + action_type="schedreboot", + details={ + "time": "2021-01-13 18:20:00", + "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", + }, + ) + + data = {"pk": action2.pk} # type: ignore + nats_cmd.return_value = "error deleting sched task" + r = self.client.delete(url, data, format="json") + self.assertEqual(r.status_code, 400) + self.assertEqual(r.data, "error deleting sched task") # type: ignore self.check_not_authenticated("delete", url) diff --git a/api/tacticalrmm/logs/urls.py b/api/tacticalrmm/logs/urls.py index 0ab067b8..3b34da5c 100644 --- a/api/tacticalrmm/logs/urls.py +++ b/api/tacticalrmm/logs/urls.py @@ -3,11 +3,9 @@ from django.urls import path from . import views urlpatterns = [ + path("pendingactions/", views.PendingActions.as_view()), path("auditlogs/", views.GetAuditLogs.as_view()), path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()), - path("/pendingactions/", views.agent_pending_actions), - path("allpendingactions/", views.all_pending_actions), - path("cancelpendingaction/", views.cancel_pending_action), path("debuglog////", views.debug_log), path("downloadlog/", views.download_log), ] diff --git a/api/tacticalrmm/logs/views.py b/api/tacticalrmm/logs/views.py index 50f52ff9..5bb74ae7 100644 --- a/api/tacticalrmm/logs/views.py +++ b/api/tacticalrmm/logs/views.py @@ -106,34 +106,38 @@ class FilterOptionsAuditLog(APIView): return Response("error", status=status.HTTP_400_BAD_REQUEST) -@api_view() -def agent_pending_actions(request, pk): - action = PendingAction.objects.filter(agent__pk=pk) - return Response(PendingActionSerializer(action, many=True).data) +class PendingActions(APIView): + def patch(self, request): + status_filter = "completed" if request.data["showCompleted"] else "pending" + completed = PendingAction.objects.filter(status="completed").count() + if "agentPK" in request.data.keys(): + actions = PendingAction.objects.filter( + agent__pk=request.data["agentPK"], status=status_filter + ) + else: + actions = PendingAction.objects.filter(status=status_filter).select_related( + "agent" + ) -@api_view() -def all_pending_actions(request): - actions = PendingAction.objects.all().select_related("agent") - return Response(PendingActionSerializer(actions, many=True).data) + ret = { + "actions": PendingActionSerializer(actions, many=True).data, + "completed_count": completed, + } + return Response(ret) + def delete(self, request): + action = get_object_or_404(PendingAction, pk=request.data["pk"]) + nats_data = { + "func": "delschedtask", + "schedtaskpayload": {"name": action.details["taskname"]}, + } + r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10)) + if r != "ok": + return notify_error(r) -@api_view(["DELETE"]) -def cancel_pending_action(request): - action = get_object_or_404(PendingAction, pk=request.data["pk"]) - if not action.agent.has_gotasks: - return notify_error("Requires agent version 1.1.1 or greater") - - nats_data = { - "func": "delschedtask", - "schedtaskpayload": {"name": action.details["taskname"]}, - } - r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10)) - if r != "ok": - return notify_error(r) - - action.delete() - return Response(f"{action.agent.hostname}: {action.description} was cancelled") + action.delete() + return Response(f"{action.agent.hostname}: {action.description} was cancelled") @api_view() diff --git a/web/src/components/modals/logs/PendingActions.vue b/web/src/components/modals/logs/PendingActions.vue index 54888c70..7ff2460d 100644 --- a/web/src/components/modals/logs/PendingActions.vue +++ b/web/src/components/modals/logs/PendingActions.vue @@ -26,7 +26,7 @@ {{ props.row.due }} {{ props.row.description }} - + { - this.actions = Object.freeze(r.data); - if (!!this.agentpk) this.hostname = r.data[0].hostname; + this.completedCount = r.data.completed_count; + this.actions = Object.freeze(r.data.actions); + if (!!this.agentpk) this.hostname = r.data.actions[0].hostname; this.$q.loading.hide(); }) .catch(e => { @@ -170,7 +178,7 @@ export default { this.$q.loading.show(); const data = { pk: this.selectedRow }; this.$axios - .delete("/logs/cancelpendingaction/", { data: data }) + .delete("/logs/pendingactions/", { data: data }) .then(r => { this.$q.loading.hide(); this.getPendingActions(); @@ -202,12 +210,6 @@ export default { }, }, computed: { - url() { - return !!this.agentpk ? `/logs/${this.agentpk}/pendingactions/` : "/logs/allpendingactions/"; - }, - filter() { - return this.showCompleted ? this.actions : this.actions.filter(k => k.status === "pending"); - }, columns() { return !!this.agentpk ? this.agent_columns : this.all_columns; }, @@ -217,9 +219,6 @@ export default { title() { return !!this.agentpk ? `Pending Actions for ${this.hostname}` : "All Pending Actions"; }, - completedCount() { - return this.actions.filter(k => k.status === "completed").length; - }, }, created() { this.getPendingActions();