diff --git a/api/tacticalrmm/agents/baker_recipes.py b/api/tacticalrmm/agents/baker_recipes.py index 26d84ac7..3cc88222 100644 --- a/api/tacticalrmm/agents/baker_recipes.py +++ b/api/tacticalrmm/agents/baker_recipes.py @@ -1,21 +1,29 @@ from .models import Agent from model_bakery.recipe import Recipe, seq +from model_bakery import baker from itertools import cycle +import datetime as dt -agent = Recipe(Agent, client="Default", site="Default", hostname="TestHostname") - -server_agent = Recipe( - Agent, - monitoring_mode="server", - client="Default", - site="Default", - hostname="ServerHost", +agent = Recipe( + Agent, + client="Default", + site="Default", + hostname=seq("TestHostname"), + monitoring_type=cycle(["workstation", "server"]), ) -workstation_agent = Recipe( - Agent, - monitoring_mode="server", - client="Default", - site="Default", - hostname="WorkstationHost", +server_agent = agent.extend( + monitoring_type="server", ) + +workstation_agent = agent.extend( + monitoring_type="workstation", +) + +online_agent = agent.extend( + last_seen=dt.datetime.now() +) + +overdue_agent = agent.extend( + last_seen=dt.datetime.now(dt.timezone.utc) - dt.timedelta(minutes=6) +) \ No newline at end of file diff --git a/api/tacticalrmm/automation/views.py b/api/tacticalrmm/automation/views.py index 4472b8d3..bf4c4fb9 100644 --- a/api/tacticalrmm/automation/views.py +++ b/api/tacticalrmm/automation/views.py @@ -498,4 +498,4 @@ class UpdatePatchPolicy(APIView): def delete(self, request, patchpolicy): get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete() - return Response("ok") \ No newline at end of file + return Response("ok") diff --git a/api/tacticalrmm/logs/admin.py b/api/tacticalrmm/logs/admin.py index e2ae5f5e..05869f9a 100644 --- a/api/tacticalrmm/logs/admin.py +++ b/api/tacticalrmm/logs/admin.py @@ -3,4 +3,4 @@ from django.contrib import admin from .models import PendingAction, AuditLog admin.site.register(PendingAction) -admin.site.register(AuditLog) \ No newline at end of file +admin.site.register(AuditLog) diff --git a/api/tacticalrmm/logs/models.py b/api/tacticalrmm/logs/models.py index c4ab06d6..bdf6d99d 100644 --- a/api/tacticalrmm/logs/models.py +++ b/api/tacticalrmm/logs/models.py @@ -168,14 +168,18 @@ class DebugLog(models.Model): class PendingAction(models.Model): agent = models.ForeignKey( - Agent, related_name="pendingactions", on_delete=models.CASCADE, + Agent, + related_name="pendingactions", + on_delete=models.CASCADE, ) entry_time = models.DateTimeField(auto_now_add=True) action_type = models.CharField( max_length=255, choices=ACTION_TYPE_CHOICES, null=True, blank=True ) status = models.CharField( - max_length=255, choices=STATUS_CHOICES, default="pending", + max_length=255, + choices=STATUS_CHOICES, + default="pending", ) celery_id = models.CharField(null=True, blank=True, max_length=255) details = models.JSONField(null=True, blank=True) diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index 3d2e5269..11027254 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -153,7 +153,11 @@ class TestAuditViews(TacticalTestCase): def test_agent_pending_actions(self): agent = baker.make_recipe("agents.agent") - pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6,) + pending_actions = baker.make( + "logs.PendingAction", + agent=agent, + _quantity=6, + ) url = f"/logs/{agent.pk}/pendingactions/" resp = self.client.get(url, format="json") diff --git a/api/tacticalrmm/tacticalrmm/test.py b/api/tacticalrmm/tacticalrmm/test.py index 0cf0f26e..9896e1ae 100644 --- a/api/tacticalrmm/tacticalrmm/test.py +++ b/api/tacticalrmm/tacticalrmm/test.py @@ -29,60 +29,6 @@ class TacticalTestCase(TestCase): def client_setup(self): self.client = APIClient() - def agent_setup(self): - self.agent = Agent.objects.create( - operating_system="Windows 10", - plat="windows", - plat_release="windows-Server2019", - hostname="DESKTOP-TEST123", - salt_id="aksdjaskdjs", - local_ip="10.0.25.188", - agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123", - services=[ - { - "pid": 880, - "name": "AeLookupSvc", - "status": "stopped", - "binpath": "C:\\Windows\\system32\\svchost.exe -k netsvcs", - "username": "localSystem", - "start_type": "manual", - "description": "Processes application compatibility cache requests for applications as they are launched", - "display_name": "Application Experience", - }, - { - "pid": 812, - "name": "ALG", - "status": "stopped", - "binpath": "C:\\Windows\\System32\\alg.exe", - "username": "NT AUTHORITY\\LocalService", - "start_type": "manual", - "description": "Provides support for 3rd party protocol plug-ins for Internet Connection Sharing", - "display_name": "Application Layer Gateway Service", - }, - ], - public_ip="74.13.24.14", - total_ram=16, - used_ram=33, - disks={ - "C:": { - "free": "42.3G", - "used": "17.1G", - "total": "59.5G", - "device": "C:", - "fstype": "NTFS", - "percent": 28, - } - }, - boot_time=8173231.4, - logged_in_username="John", - client="Google", - site="Main Office", - monitoring_type="server", - description="Test PC", - mesh_node_id="abcdefghijklmnopAABBCCDD77443355##!!AI%@#$%#*", - last_seen=djangotime.now(), - ) - # fixes tests waiting 2 minutes for mesh token to appear @override_settings(MESH_TOKEN_KEY="123456") def setup_coresettings(self): diff --git a/api/tacticalrmm/winupdate/baker_recipes.py b/api/tacticalrmm/winupdate/baker_recipes.py new file mode 100644 index 00000000..c7640b98 --- /dev/null +++ b/api/tacticalrmm/winupdate/baker_recipes.py @@ -0,0 +1,42 @@ +from itertools import cycle +from datetime import datetime as dt +import pytz +from model_bakery.recipe import Recipe, seq +from .models import WinUpdate, WinUpdatePolicy + +timezone = pytz.timezone("America/Los_Angeles") + +severity = ["Critical", "Important", "Moderate", "Low", ""] +winupdate = Recipe( + WinUpdate, + kb=seq("kb0000000"), + guid=seq("12312331-123232-123-123-1123123"), + severity=cycle(severity), +) + +approved_winupdate = winupdate.extend(action="approve") + +winupdate_policy = Recipe( + WinUpdatePolicy, + run_time_hour=dt.now(timezone).hour, + run_time_frequency="daily", + run_time_days=[dt.now(timezone).weekday()], +) + +winupdate_approve = winupdate_policy.extend( + critical="approve", + important="approve", + moderate="approve", + low="approve", + other="approve", +) + +winupdate_approve_monthly = winupdate_policy.extend( + run_time_frequency="monthly", + run_time_day=dt.now(timezone).day, + critical="approve", + important="approve", + moderate="approve", + low="approve", + other="approve", +) diff --git a/api/tacticalrmm/winupdate/models.py b/api/tacticalrmm/winupdate/models.py index 48ae7c26..6dd9487f 100644 --- a/api/tacticalrmm/winupdate/models.py +++ b/api/tacticalrmm/winupdate/models.py @@ -130,4 +130,4 @@ class WinUpdatePolicy(models.Model): # serializes the policy and returns json from .serializers import WinUpdatePolicySerializer - return WinUpdatePolicySerializer(policy).data \ No newline at end of file + return WinUpdatePolicySerializer(policy).data diff --git a/api/tacticalrmm/winupdate/tasks.py b/api/tacticalrmm/winupdate/tasks.py index e7c3e7c2..de333e05 100644 --- a/api/tacticalrmm/winupdate/tasks.py +++ b/api/tacticalrmm/winupdate/tasks.py @@ -56,9 +56,9 @@ def check_agent_update_schedule_task(): # get current time in agent local time timezone = pytz.timezone(agent.timezone) agent_localtime_now = dt.datetime.now(timezone) - weekday = int(agent_localtime_now.strftime("%w")) - hour = int(agent_localtime_now.strftime("%-H")) - day = int(agent_localtime_now.strftime("%-d")) + weekday = agent_localtime_now.weekday() + hour = agent_localtime_now.hour + day = agent_localtime_now.day if agent.patches_last_installed: # get agent last installed time in local time zone @@ -82,8 +82,7 @@ def check_agent_update_schedule_task(): if patch_policy.run_time_day > 28: months_with_30_days = [3, 6, 9, 11] - current_month = int(agent_localtime_now.strftime("%-m")) - + current_month = agent_localtime_now.month if current_month == 2: patch_policy.run_time_day = 28 elif current_month in months_with_30_days: diff --git a/api/tacticalrmm/winupdate/tests.py b/api/tacticalrmm/winupdate/tests.py index 3e19206e..cb714757 100644 --- a/api/tacticalrmm/winupdate/tests.py +++ b/api/tacticalrmm/winupdate/tests.py @@ -1,9 +1,9 @@ from tacticalrmm.test import TacticalTestCase -from .serializers import UpdateSerializer, WinUpdateSerializer, ApprovedUpdateSerializer +from .serializers import UpdateSerializer from model_bakery import baker -from model_bakery.recipe import foreign_key +from itertools import cycle from unittest.mock import patch -from pprint import pprint +from .models import WinUpdate class TestWinUpdateViews(TacticalTestCase): @@ -83,6 +83,8 @@ class TestWinUpdateViews(TacticalTestCase): resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) + self.check_not_authenticated("get", url) + def test_edit_policy(self): url = "/winupdate/editpolicy/" winupdate = baker.make("winupdate.WinUpdate") @@ -97,4 +99,106 @@ class TestWinUpdateViews(TacticalTestCase): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) -# TODO: add agent api to test \ No newline at end of file + self.check_not_authenticated("patch", url) + + +class WinupdateTasks(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + + baker.make("clients.Site", site="Default", client__client="Default") + self.online_agents = baker.make_recipe("agents.online_agent", _quantity=2) + self.offline_agent = baker.make_recipe("agents.agent") + + @patch("winupdate.tasks.check_for_updates_task.apply_async") + def test_auto_approve_task(self, check_updates_task): + from .tasks import auto_approve_updates_task + + # Setup data + baker.make_recipe( + "winupdate.winupdate", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=20, + ) + baker.make_recipe( + "winupdate.winupdate_approve", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=3, + ) + + # run task synchronously + auto_approve_updates_task() + + # make sure the check_for_updates_task was run once for each online agent + self.assertEqual(check_updates_task.call_count, 2) + + # check if all of the created updates were approved + winupdates = WinUpdate.objects.all() + for update in winupdates: + self.assertEqual(update.action, "approve") + + @patch("agents.models.Agent.salt_api_async") + def test_check_agent_update_daily_schedule(self, agent_salt_cmd): + from .tasks import check_agent_update_schedule_task + + # Setup data + # create an online agent with auto approval turned off + agent = baker.make_recipe("agents.online_agent") + baker.make("winupdate.WinUpdatePolicy", agent=agent) + + # create approved winupdates + baker.make_recipe( + "winupdate.approved_winupdate", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=20, + ) + + # create daily patch policy schedules for the agents + winupdate_policy = baker.make_recipe( + "winupdate.winupdate_approve", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=3, + ) + + check_agent_update_schedule_task() + agent_salt_cmd.assert_called_with(func="win_agent.install_updates") + self.assertEquals(agent_salt_cmd.call_count, 2) + + @patch("agents.models.Agent.salt_api_async") + def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): + from .tasks import check_agent_update_schedule_task + + # Setup data + # create an online agent with auto approval turned off + agent = baker.make_recipe("agents.online_agent") + baker.make("winupdate.WinUpdatePolicy", agent=agent) + + # create approved winupdates + baker.make_recipe( + "winupdate.approved_winupdate", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=20, + ) + + # create monthly patch policy schedules for the agents + winupdate_policy = baker.make_recipe( + "winupdate.winupdate_approve_monthly", + agent=cycle( + [self.online_agents[0], self.online_agents[1], self.offline_agent] + ), + _quantity=3, + ) + + check_agent_update_schedule_task() + agent_salt_cmd.assert_called_with(func="win_agent.install_updates") + self.assertEquals(agent_salt_cmd.call_count, 2)