diff --git a/api/tacticalrmm/software/tasks.py b/api/tacticalrmm/software/tasks.py index 0eff08a2..36ba761c 100644 --- a/api/tacticalrmm/software/tasks.py +++ b/api/tacticalrmm/software/tasks.py @@ -1,3 +1,4 @@ +import asyncio import string from time import sleep from loguru import logger @@ -89,35 +90,36 @@ def update_chocos(): @app.task def get_installed_software(pk): agent = Agent.objects.get(pk=pk) - r = agent.salt_api_cmd( - timeout=30, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) + if not agent.has_nats: + logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0") + return - if r == "timeout" or r == "error": - logger.error(f"Timed out trying to get installed software on {agent.salt_id}") + r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) + if r == "timeout" or r == "natsdown": + logger.error(f"{agent.salt_id} {r}") return printable = set(string.printable) - - try: - software = [ + sw = [] + for s in r: + sw.append( { - "name": "".join(filter(lambda x: x in printable, k)), - "version": "".join(filter(lambda x: x in printable, v)), + "name": "".join(filter(lambda x: x in printable, s["name"])), + "version": "".join(filter(lambda x: x in printable, s["version"])), + "publisher": "".join(filter(lambda x: x in printable, s["publisher"])), + "install_date": s["install_date"], + "size": s["size"], + "source": s["source"], + "location": s["location"], + "uninstall": s["uninstall"], } - for k, v in r.items() - ] - except Exception as e: - logger.error(f"Unable to get installed software on {agent.salt_id}: {e}") - return + ) if not InstalledSoftware.objects.filter(agent=agent).exists(): - InstalledSoftware(agent=agent, software=software).save() + InstalledSoftware(agent=agent, software=sw).save() else: - s = agent.installedsoftware_set.get() - s.software = software + s = agent.installedsoftware_set.first() + s.software = sw s.save(update_fields=["software"]) return "ok" diff --git a/api/tacticalrmm/software/tests.py b/api/tacticalrmm/software/tests.py index 8b92abba..79672f08 100644 --- a/api/tacticalrmm/software/tests.py +++ b/api/tacticalrmm/software/tests.py @@ -62,72 +62,6 @@ class TestSoftwareViews(TacticalTestCase): self.check_not_authenticated("get", url) - @patch("agents.models.Agent.salt_api_cmd") - def test_chocos_refresh(self, salt_api_cmd): - - salt_return = {"git": "2.3.4", "docker": "1.0.2"} - - # test a call where agent doesn't exist - resp = self.client.get("/software/refresh/500/", format="json") - self.assertEqual(resp.status_code, 404) - - agent = baker.make_recipe("agents.agent") - url = f"/software/refresh/{agent.pk}/" - - # test failed attempt - salt_api_cmd.return_value = "timeout" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) - salt_api_cmd.assert_called_with( - timeout=20, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - salt_api_cmd.reset_mock() - - salt_api_cmd.return_value = "error" - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 400) - salt_api_cmd.assert_called_with( - timeout=20, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - salt_api_cmd.reset_mock() - - # test success and created new software object - salt_api_cmd.return_value = salt_return - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 200) - salt_api_cmd.assert_called_with( - timeout=20, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists()) - salt_api_cmd.reset_mock() - - # test success and updates software object - salt_api_cmd.return_value = salt_return - resp = self.client.get(url, format="json") - self.assertEqual(resp.status_code, 200) - salt_api_cmd.assert_called_with( - timeout=20, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - software = agent.installedsoftware_set.get() - - expected = [ - {"name": "git", "version": "2.3.4"}, - {"name": "docker", "version": "1.0.2"}, - ] - - self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists()) - self.assertEquals(software.software, expected) - - self.check_not_authenticated("get", url) - class TestSoftwareTasks(TacticalTestCase): @patch("agents.models.Agent.salt_api_cmd") @@ -186,43 +120,57 @@ class TestSoftwareTasks(TacticalTestCase): salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list") self.assertEquals(salt_api_cmd.call_count, 2) - @patch("agents.models.Agent.salt_api_cmd") - def test_get_installed_software(self, salt_api_cmd): + @patch("agents.models.Agent.nats_cmd") + def test_get_installed_software(self, nats_cmd): from .tasks import get_installed_software agent = baker.make_recipe("agents.agent") - salt_return = {"git": "2.3.4", "docker": "1.0.2"} - - # test failed attempt - salt_api_cmd.return_value = "timeout" - ret = get_installed_software(agent.pk) - self.assertFalse(ret) - salt_api_cmd.assert_called_with( - timeout=30, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - salt_api_cmd.reset_mock() - - # test successful attempt - salt_api_cmd.return_value = salt_return - ret = get_installed_software(agent.pk) - self.assertTrue(ret) - salt_api_cmd.assert_called_with( - timeout=30, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) - software = agent.installedsoftware_set.get() - - expected = [ - {"name": "git", "version": "2.3.4"}, - {"name": "docker", "version": "1.0.2"}, + nats_return = [ + { + "name": "Mozilla Maintenance Service", + "size": "336.9 kB", + "source": "", + "version": "73.0.1", + "location": "", + "publisher": "Mozilla", + "uninstall": '"C:\\Program Files (x86)\\Mozilla Maintenance Service\\uninstall.exe"', + "install_date": "0001-01-01 00:00:00 +0000 UTC", + }, + { + "name": "OpenVPN 2.4.9-I601-Win10 ", + "size": "8.7 MB", + "source": "", + "version": "2.4.9-I601-Win10", + "location": "C:\\Program Files\\OpenVPN\\", + "publisher": "OpenVPN Technologies, Inc.", + "uninstall": "C:\\Program Files\\OpenVPN\\Uninstall.exe", + "install_date": "0001-01-01 00:00:00 +0000 UTC", + }, + { + "name": "Microsoft Office Professional Plus 2019 - en-us", + "size": "0 B", + "source": "", + "version": "16.0.10368.20035", + "location": "C:\\Program Files\\Microsoft Office", + "publisher": "Microsoft Corporation", + "uninstall": '"C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeClickToRun.exe" scenario=install scenariosubtype=ARP sourcetype=None productstoremove=ProPlus2019Volume.16_en-us_x-none culture=en-us version.16=16.0', + "install_date": "0001-01-01 00:00:00 +0000 UTC", + }, ] - self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists()) - self.assertEquals(software.software, expected) + # test failed attempt + nats_cmd.return_value = "timeout" + ret = get_installed_software(agent.pk) + self.assertFalse(ret) + nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=15) + nats_cmd.reset_mock() + + # test successful attempt + nats_cmd.return_value = nats_return + ret = get_installed_software(agent.pk) + self.assertTrue(ret) + nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=15) @patch("agents.models.Agent.salt_api_cmd") @patch("software.tasks.get_installed_software.delay") diff --git a/api/tacticalrmm/software/views.py b/api/tacticalrmm/software/views.py index 4d523b3e..b43d924d 100644 --- a/api/tacticalrmm/software/views.py +++ b/api/tacticalrmm/software/views.py @@ -1,3 +1,4 @@ +import asyncio import string from django.shortcuts import get_object_or_404 @@ -41,35 +42,34 @@ def get_installed(request, pk): @api_view() def refresh_installed(request, pk): agent = get_object_or_404(Agent, pk=pk) - r = agent.salt_api_cmd( - timeout=20, - func="pkg.list_pkgs", - kwargs={"include_components": False, "include_updates": False}, - ) + if not agent.has_nats: + return notify_error("Requires agent version 1.1.0 or greater") - if r == "timeout": + r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) + if r == "timeout" or r == "natsdown": return notify_error("Unable to contact the agent") - elif r == "error": - return notify_error("Something went wrong") printable = set(string.printable) - - try: - software = [ + sw = [] + for s in r: + sw.append( { - "name": "".join(filter(lambda x: x in printable, k)), - "version": "".join(filter(lambda x: x in printable, v)), + "name": "".join(filter(lambda x: x in printable, s["name"])), + "version": "".join(filter(lambda x: x in printable, s["version"])), + "publisher": "".join(filter(lambda x: x in printable, s["publisher"])), + "install_date": s["install_date"], + "size": s["size"], + "source": s["source"], + "location": s["location"], + "uninstall": s["uninstall"], } - for k, v in r.items() - ] - except Exception: - return notify_error("Something went wrong") + ) if not InstalledSoftware.objects.filter(agent=agent).exists(): - InstalledSoftware(agent=agent, software=software).save() + InstalledSoftware(agent=agent, software=sw).save() else: - s = agent.installedsoftware_set.get() - s.software = software + s = agent.installedsoftware_set.first() + s.software = sw s.save(update_fields=["software"]) return Response("ok") diff --git a/web/src/components/SoftwareTab.vue b/web/src/components/SoftwareTab.vue index 9d1eece4..a0c2244b 100644 --- a/web/src/components/SoftwareTab.vue +++ b/web/src/components/SoftwareTab.vue @@ -2,15 +2,24 @@
No agent selected
No software
- - +
+ + + + + + +
+