Compare commits
	
		
			104 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 153351cc9f | ||
|  | 1b1eec40a7 | ||
|  | 763877541a | ||
|  | 1fad7d72a2 | ||
|  | 51ea2ea879 | ||
|  | d77a478bf0 | ||
|  | e413c0264a | ||
|  | f88e7f898c | ||
|  | d07bd4a6db | ||
|  | fb34c099d5 | ||
|  | 1d2ee56a15 | ||
|  | 86665f7f09 | ||
|  | 0d2b4af986 | ||
|  | dc2b2eeb9f | ||
|  | e5dbb66d53 | ||
|  | 3474b1c471 | ||
|  | 3886de5b7c | ||
|  | 2b3cec06b3 | ||
|  | 8536754d14 | ||
|  | 1f36235801 | ||
|  | a4194b14f9 | ||
|  | 2dcc629d9d | ||
|  | 98ddadc6bc | ||
|  | f6e47b7383 | ||
|  | f073ddc906 | ||
|  | 3e00631925 | ||
|  | 9b7ac58562 | ||
|  | f242ddd801 | ||
|  | c129886fe2 | ||
|  | f577e814cf | ||
|  | c860a0cedd | ||
|  | ae7e28e492 | ||
|  | 90a63234ad | ||
|  | 14bca52e8f | ||
|  | 2f3c3361cf | ||
|  | 4034134055 | ||
|  | c04f94cb7b | ||
|  | fd1bbc7925 | ||
|  | ff69bed394 | ||
|  | d6e8c5146f | ||
|  | 9a04cf99d7 | ||
|  | 86e7c11e71 | ||
|  | 361cc08faa | ||
|  | 70dc771052 | ||
|  | c14873a799 | ||
|  | bba5abd74b | ||
|  | a224e79c1f | ||
|  | c305d98186 | ||
|  | 7c5a473e71 | ||
|  | 5e0f5d1eed | ||
|  | 238b269bc4 | ||
|  | 0ad121b9d2 | ||
|  | 7088acd9fd | ||
|  | e0a900d4b6 | ||
|  | a0fe2f0c7d | ||
|  | d5b9bc2f26 | ||
|  | 584254e6ca | ||
|  | a2963ed7bb | ||
|  | 2a3c2e133d | ||
|  | 3e7dcb2755 | ||
|  | faeec00b39 | ||
|  | eeed81392f | ||
|  | 95dce9e992 | ||
|  | 502bd2a191 | ||
|  | 17ac92a9d0 | ||
|  | ba028cde0c | ||
|  | 6e751e7a9b | ||
|  | 948b56d0e6 | ||
|  | 4bf2dc9ece | ||
|  | 125823f8ab | ||
|  | 24d33397e9 | ||
|  | 2c553825f4 | ||
|  | 198c485e9a | ||
|  | 0138505507 | ||
|  | 5d50dcc600 | ||
|  | 7bdd8c4626 | ||
|  | fc82c35f0c | ||
|  | 426ebad300 | ||
|  | 1afe61c593 | ||
|  | c20751829b | ||
|  | a3b8ee8392 | ||
|  | 156c0fe7f6 | ||
|  | 216f7a38cf | ||
|  | fd04dc10d4 | ||
|  | d39bdce926 | ||
|  | c6e01245b0 | ||
|  | c168ee7ba4 | ||
|  | 7575253000 | ||
|  | c28c1efbb1 | ||
|  | e6aa2c3b78 | ||
|  | ab7c481f83 | ||
|  | 84ad1c352d | ||
|  | e9aad39ac9 | ||
|  | c3444a87bc | ||
|  | 67b224b340 | ||
|  | bded14d36b | ||
|  | 73fa0b6631 | ||
|  | 2f07337588 | ||
|  | da163d44e7 | ||
|  | 56fbf8ae0c | ||
|  | 327eb4b39b | ||
|  | ae7873a7e3 | ||
|  | 9a5f01813b | ||
|  | 0605a3b725 | 
							
								
								
									
										5
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| .git | ||||
| .cache | ||||
| **/*.env | ||||
| **/env | ||||
| **/node_modules | ||||
							
								
								
									
										88
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| name: Publish Tactical Docker Images | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - "v*.*.*" | ||||
| jobs: | ||||
|   docker: | ||||
|     name: Build and Push Docker Images | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out the repo | ||||
|         uses: actions/checkout@v2 | ||||
|          | ||||
|       - name: Get Github Tag | ||||
|         id: prep | ||||
|         run: | | ||||
|           echo ::set-output name=version::${GITHUB_REF#refs/tags/v} | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v1 | ||||
|          | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|          | ||||
|       - name: Login to DockerHub | ||||
|         uses: docker/login-action@v1  | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|          | ||||
|       - name: Build and Push Tactical Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},tacticalrmm/tactical:latest | ||||
|            | ||||
|       - name: Build and Push Tactical MeshCentral Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-meshcentral/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},tacticalrmm/tactical-meshcentral:latest | ||||
|            | ||||
|       - name: Build and Push Tactical NATS Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-nats/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest | ||||
|            | ||||
|       - name: Build and Push Tactical Salt Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-salt/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},tacticalrmm/tactical-salt:latest | ||||
|            | ||||
|       - name: Build and Push Tactical Frontend Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-frontend/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},tacticalrmm/tactical-frontend:latest | ||||
|            | ||||
|       - name: Build and Push Tactical Nginx Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-nginx/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nginx:latest | ||||
							
								
								
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
|     "python.pythonPath": "api/tacticalrmm/env/bin/python", | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.analysis.extraPaths": [ | ||||
|         "api/tacticalrmm" | ||||
|         "api/tacticalrmm", | ||||
|     ], | ||||
|     "python.analysis.typeCheckingMode": "basic", | ||||
|     "python.formatting.provider": "black", | ||||
|   | ||||
| @@ -20,6 +20,5 @@ omit = | ||||
|     */urls.py | ||||
|     */tests.py | ||||
|     */test.py | ||||
|     api/*.py | ||||
|     checks/utils.py | ||||
|      | ||||
|   | ||||
| @@ -26,7 +26,7 @@ def get_wmi_data(): | ||||
| agent = Recipe( | ||||
|     Agent, | ||||
|     hostname="DESKTOP-TEST123", | ||||
|     version="1.1.0", | ||||
|     version="1.1.1", | ||||
|     monitoring_type=cycle(["workstation", "server"]), | ||||
|     salt_id=generate_agent_id("DESKTOP-TEST123"), | ||||
|     agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123", | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import requests | ||||
| import datetime as dt | ||||
| import time | ||||
| import base64 | ||||
| from Crypto.Cipher import AES | ||||
| @@ -8,9 +7,7 @@ from Crypto.Hash import SHA3_384 | ||||
| from Crypto.Util.Padding import pad | ||||
| import validators | ||||
| import msgpack | ||||
| import random | ||||
| import re | ||||
| import string | ||||
| from collections import Counter | ||||
| from loguru import logger | ||||
| from packaging import version as pyver | ||||
| @@ -89,6 +86,10 @@ class Agent(BaseAuditModel): | ||||
|     def has_nats(self): | ||||
|         return pyver.parse(self.version) >= pyver.parse("1.1.0") | ||||
|  | ||||
|     @property | ||||
|     def has_gotasks(self): | ||||
|         return pyver.parse(self.version) >= pyver.parse("1.1.1") | ||||
|  | ||||
|     @property | ||||
|     def timezone(self): | ||||
|         # return the default timezone unless the timezone is explicity set per agent | ||||
| @@ -163,13 +164,11 @@ class Agent(BaseAuditModel): | ||||
|                 elif i.status == "failing": | ||||
|                     failing += 1 | ||||
|  | ||||
|         has_failing_checks = True if failing > 0 else False | ||||
|  | ||||
|         ret = { | ||||
|             "total": total, | ||||
|             "passing": passing, | ||||
|             "failing": failing, | ||||
|             "has_failing_checks": has_failing_checks, | ||||
|             "has_failing_checks": failing > 0, | ||||
|         } | ||||
|         return ret | ||||
|  | ||||
| @@ -545,6 +544,7 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|         ret = AgentEditSerializer(agent).data | ||||
|         del ret["all_timezones"] | ||||
|         del ret["client"] | ||||
|         return ret | ||||
|  | ||||
|     @staticmethod | ||||
| @@ -573,61 +573,6 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|         return resp | ||||
|  | ||||
|     def schedule_reboot(self, obj): | ||||
|  | ||||
|         start_date = dt.datetime.strftime(obj, "%Y-%m-%d") | ||||
|         start_time = dt.datetime.strftime(obj, "%H:%M") | ||||
|  | ||||
|         # let windows task scheduler automatically delete the task after it runs | ||||
|         end_obj = obj + dt.timedelta(minutes=15) | ||||
|         end_date = dt.datetime.strftime(end_obj, "%Y-%m-%d") | ||||
|         end_time = dt.datetime.strftime(end_obj, "%H:%M") | ||||
|  | ||||
|         task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|             random.choice(string.ascii_letters) for _ in range(10) | ||||
|         ) | ||||
|  | ||||
|         r = self.salt_api_cmd( | ||||
|             timeout=15, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Windows\\System32\\shutdown.exe"', | ||||
|                 'arguments="/r /t 5 /f"', | ||||
|                 "trigger_type=Once", | ||||
|                 f'start_date="{start_date}"', | ||||
|                 f'start_time="{start_time}"', | ||||
|                 f'end_date="{end_date}"', | ||||
|                 f'end_time="{end_time}"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|                 "delete_after=Immediately", | ||||
|             ], | ||||
|         ) | ||||
|  | ||||
|         if r == "error" or (isinstance(r, bool) and not r): | ||||
|             return "failed" | ||||
|         elif r == "timeout": | ||||
|             return "timeout" | ||||
|         elif isinstance(r, bool) and r: | ||||
|             from logs.models import PendingAction | ||||
|  | ||||
|             details = { | ||||
|                 "taskname": task_name, | ||||
|                 "time": str(obj), | ||||
|             } | ||||
|             PendingAction(agent=self, action_type="schedreboot", details=details).save() | ||||
|  | ||||
|             nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") | ||||
|             return {"msg": {"time": nice_time, "agent": self.hostname}} | ||||
|         else: | ||||
|             return "failed" | ||||
|  | ||||
|     def not_supported(self, version_added): | ||||
|         return pyver.parse(self.version) < pyver.parse(version_added) | ||||
|  | ||||
|     def delete_superseded_updates(self): | ||||
|         try: | ||||
|             pks = []  # list of pks to delete | ||||
|   | ||||
| @@ -36,12 +36,16 @@ class AgentSerializer(serializers.ModelSerializer): | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     pending_actions = serializers.SerializerMethodField() | ||||
|     status = serializers.ReadOnlyField() | ||||
|     checks = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.SerializerMethodField() | ||||
|     client_name = serializers.ReadOnlyField(source="client.name") | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|  | ||||
|     def get_pending_actions(self, obj): | ||||
|         return obj.pendingactions.filter(status="pending").count() | ||||
|  | ||||
|     def get_last_seen(self, obj): | ||||
|         if obj.time_zone is not None: | ||||
|             agent_tz = pytz.timezone(obj.time_zone) | ||||
| @@ -62,6 +66,7 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|             "description", | ||||
|             "needs_reboot", | ||||
|             "patches_pending", | ||||
|             "pending_actions", | ||||
|             "status", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|   | ||||
| @@ -4,11 +4,10 @@ from time import sleep | ||||
| import random | ||||
| import requests | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from typing import List | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from agents.models import Agent, AgentOutage | ||||
| from core.models import CoreSettings | ||||
| @@ -16,174 +15,104 @@ from logs.models import PendingAction | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| OLD_64_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2.exe" | ||||
| OLD_32_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2-x86.exe" | ||||
|  | ||||
| def agent_update(pk: int) -> str: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     # skip if we can't determine the arch | ||||
|     if agent.arch is None: | ||||
|         logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") | ||||
|         return "noarch" | ||||
|  | ||||
|     # force an update to 1.1.5 since 1.1.6 needs agent to be on 1.1.5 first | ||||
|     if pyver.parse(agent.version) < pyver.parse("1.1.5"): | ||||
|         version = "1.1.5" | ||||
|         if agent.arch == "64": | ||||
|             url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5.exe" | ||||
|             inno = "winagent-v1.1.5.exe" | ||||
|         elif agent.arch == "32": | ||||
|             url = "https://github.com/wh1te909/rmmagent/releases/download/v1.1.5/winagent-v1.1.5-x86.exe" | ||||
|             inno = "winagent-v1.1.5-x86.exe" | ||||
|         else: | ||||
|             return "nover" | ||||
|     else: | ||||
|         version = settings.LATEST_AGENT_VER | ||||
|         url = agent.winagent_dl | ||||
|         inno = agent.win_inno_exe | ||||
|  | ||||
|     if agent.has_nats: | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).exists(): | ||||
|             action = agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).last() | ||||
|             if pyver.parse(action.details["version"]) < pyver.parse(version): | ||||
|                 action.delete() | ||||
|             else: | ||||
|                 return "pending" | ||||
|  | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent, | ||||
|             action_type="agentupdate", | ||||
|             details={ | ||||
|                 "url": url, | ||||
|                 "version": version, | ||||
|                 "inno": inno, | ||||
|             }, | ||||
|         ) | ||||
|         return "created" | ||||
|     # TODO | ||||
|     # Salt is deprecated, remove this once salt is gone | ||||
|     else: | ||||
|         agent.salt_api_async( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": inno, | ||||
|                 "url": url, | ||||
|             }, | ||||
|         ) | ||||
|         return "salt" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(pks, version): | ||||
|     assert isinstance(pks, list) | ||||
|  | ||||
| def send_agent_update_task(pks: List[int], version: str) -> None: | ||||
|     q = Agent.objects.filter(pk__in=pks) | ||||
|     agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)] | ||||
|     agents: List[int] = [ | ||||
|         i.pk for i in q if pyver.parse(i.version) < pyver.parse(version) | ||||
|     ] | ||||
|  | ||||
|     chunks = (agents[i : i + 30] for i in range(0, len(agents), 30)) | ||||
|  | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent = Agent.objects.get(pk=pk) | ||||
|  | ||||
|             # skip if we can't determine the arch | ||||
|             if agent.arch is None: | ||||
|                 logger.warning( | ||||
|                     f"Unable to determine arch on {agent.salt_id}. Skipping." | ||||
|                 ) | ||||
|                 continue | ||||
|  | ||||
|             # golang agent only backwards compatible with py agent 0.11.2 | ||||
|             # force an upgrade to the latest python agent if version < 0.11.2 | ||||
|             if pyver.parse(agent.version) < pyver.parse("0.11.2"): | ||||
|                 url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT | ||||
|                 inno = ( | ||||
|                     "winagent-v0.11.2.exe" | ||||
|                     if agent.arch == "64" | ||||
|                     else "winagent-v0.11.2-x86.exe" | ||||
|                 ) | ||||
|             else: | ||||
|                 url = agent.winagent_dl | ||||
|                 inno = agent.win_inno_exe | ||||
|             logger.info( | ||||
|                 f"Updating {agent.salt_id} current version {agent.version} using {inno}" | ||||
|             ) | ||||
|  | ||||
|             if agent.has_nats: | ||||
|                 if agent.pendingactions.filter( | ||||
|                     action_type="agentupdate", status="pending" | ||||
|                 ).exists(): | ||||
|                     continue | ||||
|  | ||||
|                 PendingAction.objects.create( | ||||
|                     agent=agent, | ||||
|                     action_type="agentupdate", | ||||
|                     details={ | ||||
|                         "url": agent.winagent_dl, | ||||
|                         "version": settings.LATEST_AGENT_VER, | ||||
|                         "inno": agent.win_inno_exe, | ||||
|                     }, | ||||
|                 ) | ||||
|             # TODO | ||||
|             # Salt is deprecated, remove this once salt is gone | ||||
|             else: | ||||
|                 r = agent.salt_api_async( | ||||
|                     func="win_agent.do_agent_update_v2", | ||||
|                     kwargs={ | ||||
|                         "inno": inno, | ||||
|                         "url": url, | ||||
|                     }, | ||||
|                 ) | ||||
|         sleep(10) | ||||
|     for pk in agents: | ||||
|         agent_update(pk) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def auto_self_agent_update_task(): | ||||
| def auto_self_agent_update_task() -> None: | ||||
|     core = CoreSettings.objects.first() | ||||
|     if not core.agent_auto_update: | ||||
|         logger.info("Agent auto update is disabled. Skipping.") | ||||
|         return | ||||
|  | ||||
|     q = Agent.objects.only("pk", "version") | ||||
|     agents = [ | ||||
|     pks: List[int] = [ | ||||
|         i.pk | ||||
|         for i in q | ||||
|         if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|     ] | ||||
|     logger.info(f"Updating {len(agents)}") | ||||
|  | ||||
|     chunks = (agents[i : i + 30] for i in range(0, len(agents), 30)) | ||||
|  | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent = Agent.objects.get(pk=pk) | ||||
|  | ||||
|             # skip if we can't determine the arch | ||||
|             if agent.arch is None: | ||||
|                 logger.warning( | ||||
|                     f"Unable to determine arch on {agent.salt_id}. Skipping." | ||||
|                 ) | ||||
|                 continue | ||||
|  | ||||
|             # golang agent only backwards compatible with py agent 0.11.2 | ||||
|             # force an upgrade to the latest python agent if version < 0.11.2 | ||||
|             if pyver.parse(agent.version) < pyver.parse("0.11.2"): | ||||
|                 url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT | ||||
|                 inno = ( | ||||
|                     "winagent-v0.11.2.exe" | ||||
|                     if agent.arch == "64" | ||||
|                     else "winagent-v0.11.2-x86.exe" | ||||
|                 ) | ||||
|             else: | ||||
|                 url = agent.winagent_dl | ||||
|                 inno = agent.win_inno_exe | ||||
|             logger.info( | ||||
|                 f"Updating {agent.salt_id} current version {agent.version} using {inno}" | ||||
|             ) | ||||
|  | ||||
|             if agent.has_nats: | ||||
|                 if agent.pendingactions.filter( | ||||
|                     action_type="agentupdate", status="pending" | ||||
|                 ).exists(): | ||||
|                     continue | ||||
|  | ||||
|                 PendingAction.objects.create( | ||||
|                     agent=agent, | ||||
|                     action_type="agentupdate", | ||||
|                     details={ | ||||
|                         "url": agent.winagent_dl, | ||||
|                         "version": settings.LATEST_AGENT_VER, | ||||
|                         "inno": agent.win_inno_exe, | ||||
|                     }, | ||||
|                 ) | ||||
|             # TODO | ||||
|             # Salt is deprecated, remove this once salt is gone | ||||
|             else: | ||||
|                 r = agent.salt_api_async( | ||||
|                     func="win_agent.do_agent_update_v2", | ||||
|                     kwargs={ | ||||
|                         "inno": inno, | ||||
|                         "url": url, | ||||
|                     }, | ||||
|                 ) | ||||
|         sleep(10) | ||||
|     for pk in pks: | ||||
|         agent_update(pk) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def update_salt_minion_task(): | ||||
|     q = Agent.objects.all() | ||||
|     agents = [ | ||||
|         i.pk | ||||
|         for i in q | ||||
|         if pyver.parse(i.version) >= pyver.parse("0.11.0") | ||||
|         and pyver.parse(i.salt_ver) < pyver.parse(settings.LATEST_SALT_VER) | ||||
| def sync_sysinfo_task(): | ||||
|     agents = Agent.objects.all() | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
|         if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online" | ||||
|     ] | ||||
|  | ||||
|     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) | ||||
|  | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent = Agent.objects.get(pk=pk) | ||||
|             r = agent.salt_api_async(func="win_agent.update_salt") | ||||
|         sleep(20) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def get_wmi_detail_task(pk): | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     if agent.has_nats: | ||||
|         asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False)) | ||||
|     else: | ||||
|         agent.salt_api_async(timeout=30, func="win_agent.local_sys_info") | ||||
|  | ||||
|     return "ok" | ||||
|     for agent in online: | ||||
|         asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -210,61 +139,43 @@ def batch_sync_modules_task(): | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def batch_sysinfo_task(): | ||||
|     # update system info using WMI | ||||
|     agents = Agent.objects.all() | ||||
|  | ||||
|     agents_nats = [agent for agent in agents if agent.has_nats] | ||||
|     minions = [ | ||||
|         agent.salt_id | ||||
|         for agent in agents | ||||
|         if not agent.has_nats and pyver.parse(agent.version) >= pyver.parse("0.11.0") | ||||
|     ] | ||||
|  | ||||
|     if minions: | ||||
|         Agent.salt_batch_async(minions=minions, func="win_agent.local_sys_info") | ||||
|  | ||||
|     for agent in agents_nats: | ||||
|         asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def uninstall_agent_task(salt_id): | ||||
| def uninstall_agent_task(salt_id, has_nats): | ||||
|     attempts = 0 | ||||
|     error = False | ||||
|  | ||||
|     while 1: | ||||
|         try: | ||||
|     if not has_nats: | ||||
|         while 1: | ||||
|             try: | ||||
|  | ||||
|             r = requests.post( | ||||
|                 f"http://{settings.SALT_HOST}:8123/run", | ||||
|                 json=[ | ||||
|                     { | ||||
|                         "client": "local", | ||||
|                         "tgt": salt_id, | ||||
|                         "fun": "win_agent.uninstall_agent", | ||||
|                         "timeout": 8, | ||||
|                         "username": settings.SALT_USERNAME, | ||||
|                         "password": settings.SALT_PASSWORD, | ||||
|                         "eauth": "pam", | ||||
|                     } | ||||
|                 ], | ||||
|                 timeout=10, | ||||
|             ) | ||||
|             ret = r.json()["return"][0][salt_id] | ||||
|         except Exception: | ||||
|             attempts += 1 | ||||
|         else: | ||||
|             if ret != "ok": | ||||
|                 r = requests.post( | ||||
|                     f"http://{settings.SALT_HOST}:8123/run", | ||||
|                     json=[ | ||||
|                         { | ||||
|                             "client": "local", | ||||
|                             "tgt": salt_id, | ||||
|                             "fun": "win_agent.uninstall_agent", | ||||
|                             "timeout": 8, | ||||
|                             "username": settings.SALT_USERNAME, | ||||
|                             "password": settings.SALT_PASSWORD, | ||||
|                             "eauth": "pam", | ||||
|                         } | ||||
|                     ], | ||||
|                     timeout=10, | ||||
|                 ) | ||||
|                 ret = r.json()["return"][0][salt_id] | ||||
|             except Exception: | ||||
|                 attempts += 1 | ||||
|             else: | ||||
|                 attempts = 0 | ||||
|                 if ret != "ok": | ||||
|                     attempts += 1 | ||||
|                 else: | ||||
|                     attempts = 0 | ||||
|  | ||||
|         if attempts >= 10: | ||||
|             error = True | ||||
|             break | ||||
|         elif attempts == 0: | ||||
|             break | ||||
|             if attempts >= 10: | ||||
|                 error = True | ||||
|                 break | ||||
|             elif attempts == 0: | ||||
|                 break | ||||
|  | ||||
|     if error: | ||||
|         logger.error(f"{salt_id} uninstall failed") | ||||
| @@ -330,19 +241,22 @@ def agent_recovery_sms_task(pk): | ||||
|  | ||||
| @app.task | ||||
| def agent_outages_task(): | ||||
|     agents = Agent.objects.only("pk") | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert" | ||||
|     ) | ||||
|  | ||||
|     for agent in agents: | ||||
|         if agent.status == "overdue": | ||||
|             outages = AgentOutage.objects.filter(agent=agent) | ||||
|             if outages and outages.last().is_active: | ||||
|                 continue | ||||
|         if agent.overdue_email_alert or agent.overdue_text_alert: | ||||
|             if agent.status == "overdue": | ||||
|                 outages = AgentOutage.objects.filter(agent=agent) | ||||
|                 if outages and outages.last().is_active: | ||||
|                     continue | ||||
|  | ||||
|             outage = AgentOutage(agent=agent) | ||||
|             outage.save() | ||||
|                 outage = AgentOutage(agent=agent) | ||||
|                 outage.save() | ||||
|  | ||||
|             if agent.overdue_email_alert and not agent.maintenance_mode: | ||||
|                 agent_outage_email_task.delay(pk=outage.pk) | ||||
|                 if agent.overdue_email_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_email_task.delay(pk=outage.pk) | ||||
|  | ||||
|             if agent.overdue_text_alert and not agent.maintenance_mode: | ||||
|                 agent_outage_sms_task.delay(pk=outage.pk) | ||||
|                 if agent.overdue_text_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_sms_task.delay(pk=outage.pk) | ||||
|   | ||||
| @@ -5,22 +5,20 @@ from unittest.mock import patch | ||||
| from model_bakery import baker | ||||
| from itertools import cycle | ||||
|  | ||||
| from django.test import TestCase, override_settings | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import PendingAction | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from .serializers import AgentSerializer | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from .models import Agent | ||||
| from .tasks import ( | ||||
|     agent_recovery_sms_task, | ||||
|     auto_self_agent_update_task, | ||||
|     update_salt_minion_task, | ||||
|     get_wmi_detail_task, | ||||
|     sync_salt_modules_task, | ||||
|     batch_sync_modules_task, | ||||
|     batch_sysinfo_task, | ||||
|     OLD_64_PY_AGENT, | ||||
|     OLD_32_PY_AGENT, | ||||
| ) | ||||
| from winupdate.models import WinUpdatePolicy | ||||
|  | ||||
| @@ -33,7 +31,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|         client = baker.make("clients.Client", name="Google") | ||||
|         site = baker.make("clients.Site", client=client, name="LA Office") | ||||
|         self.agent = baker.make_recipe( | ||||
|             "agents.online_agent", site=site, version="1.1.0" | ||||
|             "agents.online_agent", site=site, version="1.1.1" | ||||
|         ) | ||||
|         baker.make_recipe("winupdate.winupdate_policy", agent=self.agent) | ||||
|  | ||||
| @@ -123,7 +121,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         nats_cmd.assert_called_with({"func": "uninstall"}, wait=False) | ||||
|         reload_nats.assert_called_once() | ||||
|         mock_task.assert_called_with(self.agent.salt_id) | ||||
|         mock_task.assert_called_with(self.agent.salt_id, True) | ||||
|  | ||||
|         self.check_not_authenticated("delete", url) | ||||
|  | ||||
| @@ -186,10 +184,10 @@ class TestAgentViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_power_action(self, nats_cmd): | ||||
|         url = f"/agents/poweraction/" | ||||
|     def test_reboot_now(self, nats_cmd): | ||||
|         url = f"/agents/reboot/" | ||||
|  | ||||
|         data = {"pk": self.agent.pk, "action": "rebootnow"} | ||||
|         data = {"pk": self.agent.pk} | ||||
|         nats_cmd.return_value = "ok" | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
| @@ -222,30 +220,37 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_reboot_later(self, mock_ret): | ||||
|         url = f"/agents/rebootlater/" | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_reboot_later(self, nats_cmd): | ||||
|         url = f"/agents/reboot/" | ||||
|  | ||||
|         data = { | ||||
|             "pk": self.agent.pk, | ||||
|             "datetime": "2025-08-29 18:41", | ||||
|         } | ||||
|  | ||||
|         mock_ret.return_value = True | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         nats_cmd.return_value = "ok" | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") | ||||
|         self.assertEqual(r.data["agent"], self.agent.hostname) | ||||
|  | ||||
|         mock_ret.return_value = "failed" | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         nats_data = { | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "schedreboot", | ||||
|                 "trigger": "once", | ||||
|                 "name": r.data["task_name"], | ||||
|                 "year": 2025, | ||||
|                 "month": "August", | ||||
|                 "day": 29, | ||||
|                 "hour": 18, | ||||
|                 "min": 41, | ||||
|             }, | ||||
|         } | ||||
|         nats_cmd.assert_called_with(nats_data, timeout=10) | ||||
|  | ||||
|         mock_ret.return_value = "timeout" | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         mock_ret.return_value = False | ||||
|         nats_cmd.return_value = "error creating task" | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
| @@ -253,12 +258,12 @@ class TestAgentViews(TacticalTestCase): | ||||
|             "pk": self.agent.pk, | ||||
|             "datetime": "rm -rf /", | ||||
|         } | ||||
|         r = self.client.post(url, data_invalid, format="json") | ||||
|         r = self.client.patch(url, data_invalid, format="json") | ||||
|  | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "Invalid date") | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
|     @patch("os.path.exists") | ||||
|     @patch("subprocess.run") | ||||
| @@ -428,7 +433,14 @@ class TestAgentViews(TacticalTestCase): | ||||
|         self.assertIn("&viewmode=13", r.data["file"]) | ||||
|         self.assertIn("&viewmode=12", r.data["terminal"]) | ||||
|         self.assertIn("&viewmode=11", r.data["control"]) | ||||
|         self.assertIn("mstsc.html?login=", r.data["webrdp"]) | ||||
|  | ||||
|         self.assertIn("&gotonode=", r.data["file"]) | ||||
|         self.assertIn("&gotonode=", r.data["terminal"]) | ||||
|         self.assertIn("&gotonode=", r.data["control"]) | ||||
|  | ||||
|         self.assertIn("?login=", r.data["file"]) | ||||
|         self.assertIn("?login=", r.data["terminal"]) | ||||
|         self.assertIn("?login=", r.data["control"]) | ||||
|  | ||||
|         self.assertEqual(self.agent.hostname, r.data["hostname"]) | ||||
|         self.assertEqual(self.agent.client.name, r.data["client"]) | ||||
| @@ -739,19 +751,6 @@ class TestAgentTasks(TacticalTestCase): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     @patch("agents.models.Agent.salt_api_async", return_value=None) | ||||
|     def test_get_wmi_detail_task(self, salt_api_async, nats_cmd): | ||||
|         self.agent_salt = baker.make_recipe("agents.agent", version="1.0.2") | ||||
|         ret = get_wmi_detail_task.s(self.agent_salt.pk).apply() | ||||
|         salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info") | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         self.agent_nats = baker.make_recipe("agents.agent", version="1.1.0") | ||||
|         ret = get_wmi_detail_task.s(self.agent_nats.pk).apply() | ||||
|         nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_sync_salt_modules_task(self, salt_api_cmd): | ||||
|         self.agent = baker.make_recipe("agents.agent") | ||||
| @@ -787,84 +786,71 @@ class TestAgentTasks(TacticalTestCase): | ||||
|         self.assertEqual(salt_batch_async.call_count, 4) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     @patch("agents.models.Agent.salt_batch_async", return_value=None) | ||||
|     @patch("agents.tasks.sleep", return_value=None) | ||||
|     def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async, nats_cmd): | ||||
|  | ||||
|         self.agents_nats = baker.make_recipe( | ||||
|             "agents.agent", version="1.1.0", _quantity=20 | ||||
|         ) | ||||
|         # test nats | ||||
|         ret = batch_sysinfo_task.s().apply() | ||||
|         self.assertEqual(nats_cmd.call_count, 20) | ||||
|         nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         self.agents_salt = baker.make_recipe( | ||||
|             "agents.agent", version="1.0.2", _quantity=70 | ||||
|         ) | ||||
|  | ||||
|         minions = [i.salt_id for i in self.agents_salt] | ||||
|  | ||||
|         ret = batch_sysinfo_task.s().apply() | ||||
|         self.assertEqual(salt_batch_async.call_count, 1) | ||||
|         salt_batch_async.assert_called_with( | ||||
|             minions=minions, func="win_agent.local_sys_info" | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         salt_batch_async.reset_mock() | ||||
|         [i.delete() for i in self.agents_salt] | ||||
|  | ||||
|         # test old agents, should not run | ||||
|         self.agents_old = baker.make_recipe( | ||||
|             "agents.agent", version="0.10.2", _quantity=70 | ||||
|         ) | ||||
|         ret = batch_sysinfo_task.s().apply() | ||||
|         salt_batch_async.assert_not_called() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_async", return_value=None) | ||||
|     @patch("agents.tasks.sleep", return_value=None) | ||||
|     def test_update_salt_minion_task(self, mock_sleep, salt_api_async): | ||||
|         # test agents that need salt update | ||||
|         self.agents = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             version=settings.LATEST_AGENT_VER, | ||||
|             salt_ver="1.0.3", | ||||
|             _quantity=53, | ||||
|         ) | ||||
|         ret = update_salt_minion_task.s().apply() | ||||
|         self.assertEqual(salt_api_async.call_count, 53) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         [i.delete() for i in self.agents] | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test agents that need salt update but agent version too low | ||||
|         self.agents = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             version="0.10.2", | ||||
|             salt_ver="1.0.3", | ||||
|             _quantity=53, | ||||
|         ) | ||||
|         ret = update_salt_minion_task.s().apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         salt_api_async.assert_not_called() | ||||
|         [i.delete() for i in self.agents] | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         # test agents already on latest salt ver | ||||
|         self.agents = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             version=settings.LATEST_AGENT_VER, | ||||
|             salt_ver=settings.LATEST_SALT_VER, | ||||
|             _quantity=53, | ||||
|         ) | ||||
|         ret = update_salt_minion_task.s().apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         salt_api_async.assert_not_called() | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_async") | ||||
|     def test_agent_update(self, salt_api_async): | ||||
|         from agents.tasks import agent_update | ||||
|  | ||||
|         agent_noarch = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Error getting OS", | ||||
|             version="1.1.0", | ||||
|         ) | ||||
|         r = agent_update(agent_noarch.pk) | ||||
|         self.assertEqual(r, "noarch") | ||||
|         self.assertEqual( | ||||
|             PendingAction.objects.filter( | ||||
|                 agent=agent_noarch, action_type="agentupdate" | ||||
|             ).count(), | ||||
|             0, | ||||
|         ) | ||||
|  | ||||
|         agent64_nats = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.1.0", | ||||
|         ) | ||||
|  | ||||
|         r = agent_update(agent64_nats.pk) | ||||
|         self.assertEqual(r, "created") | ||||
|         action = PendingAction.objects.get(agent__pk=agent64_nats.pk) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") | ||||
|         self.assertEqual(action.details["url"], settings.DL_64) | ||||
|         self.assertEqual( | ||||
|             action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe" | ||||
|         ) | ||||
|         self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER) | ||||
|  | ||||
|         agent64_salt = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 10 Pro, 64 bit (build 19041.450)", | ||||
|             version="1.0.0", | ||||
|         ) | ||||
|         salt_api_async.return_value = True | ||||
|         r = agent_update(agent64_salt.pk) | ||||
|         self.assertEqual(r, "salt") | ||||
|         salt_api_async.assert_called_with( | ||||
|             func="win_agent.do_agent_update_v2", | ||||
|             kwargs={ | ||||
|                 "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe", | ||||
|                 "url": settings.DL_64, | ||||
|             }, | ||||
|         ) | ||||
|         salt_api_async.reset_mock() | ||||
|  | ||||
|         agent32_nats = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 7 Professional, 32 bit (build 7601.23964)", | ||||
|             version="1.1.0", | ||||
|         ) | ||||
|  | ||||
|         agent32_salt = baker.make_recipe( | ||||
|             "agents.agent", | ||||
|             operating_system="Windows 7 Professional, 32 bit (build 7601.23964)", | ||||
|             version="1.0.0", | ||||
|         ) | ||||
|  | ||||
|     """ @patch("agents.models.Agent.salt_api_async") | ||||
|     @patch("agents.tasks.sleep", return_value=None) | ||||
|     def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): | ||||
|         # test 64bit golang agent | ||||
| @@ -967,4 +953,4 @@ class TestAgentTasks(TacticalTestCase): | ||||
|                 "url": OLD_32_PY_AGENT, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.assertEqual(ret.status, "SUCCESS") """ | ||||
| @@ -12,7 +12,6 @@ urlpatterns = [ | ||||
|     path("<pk>/agentdetail/", views.agent_detail), | ||||
|     path("<int:pk>/meshcentral/", views.meshcentral), | ||||
|     path("<str:arch>/getmeshexe/", views.get_mesh_exe), | ||||
|     path("poweraction/", views.power_action), | ||||
|     path("uninstall/", views.uninstall), | ||||
|     path("editagent/", views.edit_agent), | ||||
|     path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log), | ||||
| @@ -20,7 +19,7 @@ urlpatterns = [ | ||||
|     path("updateagents/", views.update_agents), | ||||
|     path("<pk>/getprocs/", views.get_processes), | ||||
|     path("<pk>/<pid>/killproc/", views.kill_proc), | ||||
|     path("rebootlater/", views.reboot_later), | ||||
|     path("reboot/", views.Reboot.as_view()), | ||||
|     path("installagent/", views.install_agent), | ||||
|     path("<int:pk>/ping/", views.ping), | ||||
|     path("recover/", views.recover), | ||||
| @@ -31,4 +30,5 @@ urlpatterns = [ | ||||
|     path("bulk/", views.bulk), | ||||
|     path("agent_counts/", views.agent_counts), | ||||
|     path("maintenance/", views.agent_maintenance), | ||||
|     path("<int:pk>/wmi/", views.WMI.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -3,6 +3,8 @@ from loguru import logger | ||||
| import os | ||||
| import subprocess | ||||
| import pytz | ||||
| import random | ||||
| import string | ||||
| import datetime as dt | ||||
| from packaging import version as pyver | ||||
|  | ||||
| @@ -18,7 +20,7 @@ from rest_framework import status, generics | ||||
| from .models import Agent, AgentOutage, RecoveryAction, Note | ||||
| from core.models import CoreSettings | ||||
| from scripts.models import Script | ||||
| from logs.models import AuditLog | ||||
| from logs.models import AuditLog, PendingAction | ||||
|  | ||||
| from .serializers import ( | ||||
|     AgentSerializer, | ||||
| @@ -61,38 +63,40 @@ def update_agents(request): | ||||
| @api_view() | ||||
| def ping(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=10)) | ||||
|     status = "offline" | ||||
|     if agent.has_nats: | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5)) | ||||
|         if r == "pong": | ||||
|             status = "online" | ||||
|     else: | ||||
|         r = agent.salt_api_cmd(timeout=5, func="test.ping") | ||||
|         if isinstance(r, bool) and r: | ||||
|             status = "online" | ||||
|  | ||||
|     if r == "timeout" or r == "natsdown": | ||||
|         return Response({"name": agent.hostname, "status": "offline"}) | ||||
|     elif r == "pong": | ||||
|         return Response({"name": agent.hostname, "status": "online"}) | ||||
|  | ||||
|     return Response({"name": agent.hostname, "status": "offline"}) | ||||
|     return Response({"name": agent.hostname, "status": status}) | ||||
|  | ||||
|  | ||||
| @api_view(["DELETE"]) | ||||
| def uninstall(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|     asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|     if agent.has_nats: | ||||
|         asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|  | ||||
|     salt_id = agent.salt_id | ||||
|     name = agent.hostname | ||||
|     has_nats = agent.has_nats | ||||
|     agent.delete() | ||||
|     reload_nats() | ||||
|  | ||||
|     uninstall_agent_task.delay(salt_id) | ||||
|     uninstall_agent_task.delay(salt_id, has_nats) | ||||
|     return Response(f"{name} will now be uninstalled.") | ||||
|  | ||||
|  | ||||
| @api_view(["PATCH"]) | ||||
| def edit_agent(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["id"]) | ||||
|  | ||||
|     old_site = agent.site.pk | ||||
|     a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|     a_serializer.is_valid(raise_exception=True) | ||||
|     a_serializer.save() | ||||
| @@ -104,6 +108,11 @@ def edit_agent(request): | ||||
|     p_serializer.is_valid(raise_exception=True) | ||||
|     p_serializer.save() | ||||
|  | ||||
|     # check if site changed and initiate generating correct policies | ||||
|     if old_site != request.data["site"]: | ||||
|         agent.generate_checks_from_policies(clear=True) | ||||
|         agent.generate_tasks_from_policies(clear=True) | ||||
|  | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| @@ -119,16 +128,9 @@ def meshcentral(request, pk): | ||||
|     if token == "err": | ||||
|         return notify_error("Invalid mesh token") | ||||
|  | ||||
|     control = ( | ||||
|         f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=11&hide=31" | ||||
|     ) | ||||
|     terminal = ( | ||||
|         f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=12&hide=31" | ||||
|     ) | ||||
|     file = ( | ||||
|         f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=13&hide=31" | ||||
|     ) | ||||
|     webrdp = f"{core.mesh_site}/mstsc.html?login={token}&node={agent.mesh_node_id}" | ||||
|     control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" | ||||
|     terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" | ||||
|     file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" | ||||
|  | ||||
|     AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname) | ||||
|  | ||||
| @@ -137,7 +139,6 @@ def meshcentral(request, pk): | ||||
|         "control": control, | ||||
|         "terminal": terminal, | ||||
|         "file": file, | ||||
|         "webrdp": webrdp, | ||||
|         "status": agent.status, | ||||
|         "client": agent.client.name, | ||||
|         "site": agent.site.name, | ||||
| @@ -201,19 +202,6 @@ def get_event_log(request, pk, logtype, days): | ||||
|     return Response(r) | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def power_action(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|     if request.data["action"] == "rebootnow": | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10)) | ||||
|         if r != "ok": | ||||
|             return notify_error("Unable to contact the agent") | ||||
|  | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def send_raw_cmd(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
| @@ -372,24 +360,63 @@ def overdue_action(request): | ||||
|     return Response(agent.hostname) | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def reboot_later(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|     date_time = request.data["datetime"] | ||||
| class Reboot(APIView): | ||||
|     # reboot now | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|         if not agent.has_nats: | ||||
|             return notify_error("Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|     try: | ||||
|         obj = dt.datetime.strptime(date_time, "%Y-%m-%d %H:%M") | ||||
|     except Exception: | ||||
|         return notify_error("Invalid date") | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10)) | ||||
|         if r != "ok": | ||||
|             return notify_error("Unable to contact the agent") | ||||
|  | ||||
|     r = agent.schedule_reboot(obj) | ||||
|         return Response("ok") | ||||
|  | ||||
|     if r == "timeout": | ||||
|         return notify_error("Unable to contact the agent") | ||||
|     elif r == "failed": | ||||
|         return notify_error("Something went wrong") | ||||
|     # reboot later | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|         if not agent.has_gotasks: | ||||
|             return notify_error("Requires agent version 1.1.1 or greater") | ||||
|  | ||||
|     return Response(r["msg"]) | ||||
|         try: | ||||
|             obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M") | ||||
|         except Exception: | ||||
|             return notify_error("Invalid date") | ||||
|  | ||||
|         task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|             random.choice(string.ascii_letters) for _ in range(10) | ||||
|         ) | ||||
|  | ||||
|         nats_data = { | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "schedreboot", | ||||
|                 "trigger": "once", | ||||
|                 "name": task_name, | ||||
|                 "year": int(dt.datetime.strftime(obj, "%Y")), | ||||
|                 "month": dt.datetime.strftime(obj, "%B"), | ||||
|                 "day": int(dt.datetime.strftime(obj, "%d")), | ||||
|                 "hour": int(dt.datetime.strftime(obj, "%H")), | ||||
|                 "min": int(dt.datetime.strftime(obj, "%M")), | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         if pyver.parse(agent.version) >= pyver.parse("1.1.2"): | ||||
|             nats_data["schedtaskpayload"]["deleteafter"] = True | ||||
|  | ||||
|         r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) | ||||
|         if r != "ok": | ||||
|             return notify_error(r) | ||||
|  | ||||
|         details = {"taskname": task_name, "time": str(obj)} | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent, action_type="schedreboot", details=details | ||||
|         ) | ||||
|         nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") | ||||
|         return Response( | ||||
|             {"time": nice_time, "agent": agent.hostname, "task_name": task_name} | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| @@ -550,7 +577,7 @@ def install_agent(request): | ||||
|             "&&", | ||||
|             "timeout", | ||||
|             "/t", | ||||
|             "20", | ||||
|             "10", | ||||
|             "/nobreak", | ||||
|             ">", | ||||
|             "NUL", | ||||
| @@ -871,3 +898,15 @@ def agent_maintenance(request): | ||||
|         return notify_error("Invalid data") | ||||
|  | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| class WMI(APIView): | ||||
|     def get(self, request, pk): | ||||
|         agent = get_object_or_404(Agent, pk=pk) | ||||
|         if pyver.parse(agent.version) < pyver.parse("1.1.2"): | ||||
|             return notify_error("Requires agent version 1.1.2 or greater") | ||||
|  | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20)) | ||||
|         if r != "ok": | ||||
|             return notify_error("Unable to contact the agent") | ||||
|         return Response("ok") | ||||
| @@ -1,5 +0,0 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class ApiConfig(AppConfig): | ||||
|     name = "api" | ||||
| @@ -1,11 +0,0 @@ | ||||
| from django.urls import path | ||||
| from . import views | ||||
| from apiv3 import views as v3_views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("triggerpatchscan/", views.trigger_patch_scan), | ||||
|     path("<int:pk>/checkrunner/", views.CheckRunner.as_view()), | ||||
|     path("<int:pk>/taskrunner/", views.TaskRunner.as_view()), | ||||
|     path("<int:pk>/saltinfo/", views.SaltInfo.as_view()), | ||||
|     path("<int:pk>/meshinfo/", v3_views.MeshInfo.as_view()), | ||||
| ] | ||||
| @@ -1,149 +0,0 @@ | ||||
| from loguru import logger | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from rest_framework.authentication import TokenAuthentication | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.decorators import ( | ||||
|     api_view, | ||||
|     authentication_classes, | ||||
|     permission_classes, | ||||
| ) | ||||
|  | ||||
| from agents.models import Agent | ||||
| from checks.models import Check | ||||
| from autotasks.models import AutomatedTask | ||||
|  | ||||
| from winupdate.tasks import check_for_updates_task | ||||
|  | ||||
| from autotasks.serializers import TaskRunnerGetSerializer, TaskRunnerPatchSerializer | ||||
| from checks.serializers import CheckRunnerGetSerializer, CheckResultsSerializer | ||||
|  | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| @api_view(["PATCH"]) | ||||
| @authentication_classes((TokenAuthentication,)) | ||||
| @permission_classes((IsAuthenticated,)) | ||||
| def trigger_patch_scan(request): | ||||
|     agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|     reboot_policy = agent.get_patch_policy().reboot_after_install | ||||
|     reboot = False | ||||
|  | ||||
|     if reboot_policy == "always": | ||||
|         reboot = True | ||||
|  | ||||
|     if request.data["reboot"]: | ||||
|         if reboot_policy == "required": | ||||
|             reboot = True | ||||
|         elif reboot_policy == "never": | ||||
|             agent.needs_reboot = True | ||||
|             agent.save(update_fields=["needs_reboot"]) | ||||
|  | ||||
|     if reboot: | ||||
|         r = agent.salt_api_cmd( | ||||
|             timeout=15, | ||||
|             func="system.reboot", | ||||
|             arg=7, | ||||
|             kwargs={"in_seconds": True}, | ||||
|         ) | ||||
|  | ||||
|         if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): | ||||
|             check_for_updates_task.apply_async( | ||||
|                 queue="wupdate", kwargs={"pk": agent.pk, "wait": False} | ||||
|             ) | ||||
|         else: | ||||
|             logger.info(f"{agent.hostname} is rebooting after updates were installed.") | ||||
|     else: | ||||
|         check_for_updates_task.apply_async( | ||||
|             queue="wupdate", kwargs={"pk": agent.pk, "wait": False} | ||||
|         ) | ||||
|  | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| class CheckRunner(APIView): | ||||
|     """ | ||||
|     For windows agent | ||||
|     """ | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         agent = get_object_or_404(Agent, pk=pk) | ||||
|         checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False) | ||||
|  | ||||
|         ret = { | ||||
|             "agent": agent.pk, | ||||
|             "check_interval": agent.check_interval, | ||||
|             "checks": CheckRunnerGetSerializer(checks, many=True).data, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
|     def patch(self, request, pk): | ||||
|         check = get_object_or_404(Check, pk=pk) | ||||
|  | ||||
|         if check.check_type != "cpuload" and check.check_type != "memory": | ||||
|             serializer = CheckResultsSerializer( | ||||
|                 instance=check, data=request.data, partial=True | ||||
|             ) | ||||
|             serializer.is_valid(raise_exception=True) | ||||
|             serializer.save(last_run=djangotime.now()) | ||||
|  | ||||
|         else: | ||||
|             check.last_run = djangotime.now() | ||||
|             check.save(update_fields=["last_run"]) | ||||
|  | ||||
|         check.handle_check(request.data) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class TaskRunner(APIView): | ||||
|     """ | ||||
|     For the windows python agent | ||||
|     """ | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|  | ||||
|         task = get_object_or_404(AutomatedTask, pk=pk) | ||||
|         return Response(TaskRunnerGetSerializer(task).data) | ||||
|  | ||||
|     def patch(self, request, pk): | ||||
|         task = get_object_or_404(AutomatedTask, pk=pk) | ||||
|  | ||||
|         serializer = TaskRunnerPatchSerializer( | ||||
|             instance=task, data=request.data, partial=True | ||||
|         ) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save(last_run=djangotime.now()) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SaltInfo(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         agent = get_object_or_404(Agent, pk=pk) | ||||
|         ret = { | ||||
|             "latestVer": settings.LATEST_SALT_VER, | ||||
|             "currentVer": agent.salt_ver, | ||||
|             "salt_id": agent.salt_id, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
|     def patch(self, request, pk): | ||||
|         agent = get_object_or_404(Agent, pk=pk) | ||||
|         agent.salt_ver = request.data["ver"] | ||||
|         agent.save(update_fields=["salt_ver"]) | ||||
|         return Response("ok") | ||||
| @@ -45,15 +45,11 @@ class TestAPIv3(TacticalTestCase): | ||||
|  | ||||
|     def test_get_mesh_info(self): | ||||
|         url = f"/api/v3/{self.agent.pk}/meshinfo/" | ||||
|         url2 = f"/api/v1/{self.agent.pk}/meshinfo/" | ||||
|  | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         r = self.client.get(url2) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|         self.check_not_authenticated("get", url2) | ||||
|  | ||||
|     def test_get_winupdater(self): | ||||
|         url = f"/api/v3/{self.agent.agent_id}/winupdater/" | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from django.urls import path | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("checkin/", views.CheckIn.as_view()), | ||||
|     path("hello/", views.Hello.as_view()), | ||||
|     path("checkrunner/", views.CheckRunner.as_view()), | ||||
|     path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()), | ||||
| @@ -14,4 +15,6 @@ urlpatterns = [ | ||||
|     path("newagent/", views.NewAgent.as_view()), | ||||
|     path("winupdater/", views.WinUpdater.as_view()), | ||||
|     path("<str:agentid>/winupdater/", views.WinUpdater.as_view()), | ||||
|     path("software/", views.Software.as_view()), | ||||
|     path("installer/", views.Installer.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -2,12 +2,12 @@ import asyncio | ||||
| import os | ||||
| import requests | ||||
| from loguru import logger | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from django.http import HttpResponse | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| @@ -20,6 +20,7 @@ from checks.models import Check | ||||
| from autotasks.models import AutomatedTask | ||||
| from accounts.models import User | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from software.models import InstalledSoftware | ||||
| from checks.serializers import CheckRunnerGetSerializerV3 | ||||
| from agents.serializers import WinAgentSerializer | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||
| @@ -28,18 +29,120 @@ from winupdate.serializers import ApprovedUpdateSerializer | ||||
| from agents.tasks import ( | ||||
|     agent_recovery_email_task, | ||||
|     agent_recovery_sms_task, | ||||
|     get_wmi_detail_task, | ||||
|     sync_salt_modules_task, | ||||
| ) | ||||
| from winupdate.tasks import check_for_updates_task | ||||
| from software.tasks import get_installed_software, install_chocolatey | ||||
| from software.tasks import install_chocolatey | ||||
| from checks.utils import bytes2human | ||||
| from tacticalrmm.utils import notify_error, reload_nats | ||||
| from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| class CheckIn(APIView): | ||||
|     """ | ||||
|     The agent's checkin endpoint | ||||
|     patch: called every 45 to 110 seconds, handles agent updates and recovery | ||||
|     put: called every 5 to 10 minutes, handles basic system info | ||||
|     post: called once on windows service startup | ||||
|     """ | ||||
|  | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent.version = request.data["version"] | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["version", "last_seen"]) | ||||
|  | ||||
|         if agent.agentoutages.exists() and agent.agentoutages.last().is_active: | ||||
|             last_outage = agent.agentoutages.last() | ||||
|             last_outage.recovery_time = djangotime.now() | ||||
|             last_outage.save(update_fields=["recovery_time"]) | ||||
|  | ||||
|             if agent.overdue_email_alert: | ||||
|                 agent_recovery_email_task.delay(pk=last_outage.pk) | ||||
|             if agent.overdue_text_alert: | ||||
|                 agent_recovery_sms_task.delay(pk=last_outage.pk) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last() | ||||
|         if recovery is not None: | ||||
|             recovery.last_run = djangotime.now() | ||||
|             recovery.save(update_fields=["last_run"]) | ||||
|             return Response(recovery.send()) | ||||
|  | ||||
|         # handle agent update | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).exists(): | ||||
|             update = agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).last() | ||||
|             update.status = "completed" | ||||
|             update.save(update_fields=["status"]) | ||||
|             return Response(update.details) | ||||
|  | ||||
|         # get any pending actions | ||||
|         if agent.pendingactions.filter(status="pending").exists(): | ||||
|             agent.handle_pending_actions() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|  | ||||
|         disks = request.data["disks"] | ||||
|         new = [] | ||||
|         # python agent | ||||
|         if isinstance(disks, dict): | ||||
|             for k, v in disks.items(): | ||||
|                 new.append(v) | ||||
|         else: | ||||
|             # golang agent | ||||
|             for disk in disks: | ||||
|                 tmp = {} | ||||
|                 for k, v in disk.items(): | ||||
|                     tmp["device"] = disk["device"] | ||||
|                     tmp["fstype"] = disk["fstype"] | ||||
|                     tmp["total"] = bytes2human(disk["total"]) | ||||
|                     tmp["used"] = bytes2human(disk["used"]) | ||||
|                     tmp["free"] = bytes2human(disk["free"]) | ||||
|                     tmp["percent"] = int(disk["percent"]) | ||||
|                 new.append(tmp) | ||||
|  | ||||
|         if request.data["logged_in_username"] == "None": | ||||
|             serializer.save(disks=new) | ||||
|         else: | ||||
|             serializer.save( | ||||
|                 disks=new, | ||||
|                 last_logged_in_user=request.data["logged_in_username"], | ||||
|             ) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|  | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save(last_seen=djangotime.now()) | ||||
|  | ||||
|         sync_salt_modules_task.delay(agent.pk) | ||||
|         check_for_updates_task.apply_async( | ||||
|             queue="wupdate", kwargs={"pk": agent.pk, "wait": True} | ||||
|         ) | ||||
|  | ||||
|         if not agent.choco_installed: | ||||
|             install_chocolatey.delay(agent.pk, wait=True) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class Hello(APIView): | ||||
|     #### DEPRECATED, for agents <= 1.1.9 #### | ||||
|     """ | ||||
|     The agent's checkin endpoint | ||||
|     patch: called every 30 to 120 seconds | ||||
| @@ -123,8 +226,6 @@ class Hello(APIView): | ||||
|         serializer.save(last_seen=djangotime.now()) | ||||
|  | ||||
|         sync_salt_modules_task.delay(agent.pk) | ||||
|         get_installed_software.delay(agent.pk) | ||||
|         get_wmi_detail_task.delay(agent.pk) | ||||
|         check_for_updates_task.apply_async( | ||||
|             queue="wupdate", kwargs={"pk": agent.pk, "wait": True} | ||||
|         ) | ||||
| @@ -386,7 +487,15 @@ class MeshInfo(APIView): | ||||
|  | ||||
|     def patch(self, request, pk): | ||||
|         agent = get_object_or_404(Agent, pk=pk) | ||||
|         agent.mesh_node_id = request.data["nodeidhex"] | ||||
|  | ||||
|         if "nodeidhex" in request.data: | ||||
|             # agent <= 1.1.0 | ||||
|             nodeid = request.data["nodeidhex"] | ||||
|         else: | ||||
|             # agent >= 1.1.1 | ||||
|             nodeid = request.data["nodeid"] | ||||
|  | ||||
|         agent.mesh_node_id = nodeid | ||||
|         agent.save(update_fields=["mesh_node_id"]) | ||||
|         return Response("ok") | ||||
|  | ||||
| @@ -476,3 +585,42 @@ class NewAgent(APIView): | ||||
|                 "token": token.key, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class Software(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         raw: SoftwareList = request.data["software"] | ||||
|         if not isinstance(raw, list): | ||||
|             return notify_error("err") | ||||
|  | ||||
|         sw = filter_software(raw) | ||||
|         if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|             InstalledSoftware(agent=agent, software=sw).save() | ||||
|         else: | ||||
|             s = agent.installedsoftware_set.first() | ||||
|             s.software = sw | ||||
|             s.save(update_fields=["software"]) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class Installer(APIView): | ||||
|     def get(self, request): | ||||
|         # used to check if token is valid. will return 401 if not | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
|         if "version" not in request.data: | ||||
|             return notify_error("Invalid data") | ||||
|  | ||||
|         ver = request.data["version"] | ||||
|         if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER): | ||||
|             return notify_error( | ||||
|                 f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM" | ||||
|             ) | ||||
|  | ||||
|         return Response("ok") | ||||
|   | ||||
| @@ -1051,10 +1051,13 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|         for task in tasks: | ||||
|             run_win_task.assert_any_call(task.id) | ||||
|  | ||||
|     def test_update_policy_tasks(self): | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_update_policy_tasks(self, nats_cmd): | ||||
|         from .tasks import update_policy_task_fields_task | ||||
|         from autotasks.models import AutomatedTask | ||||
|  | ||||
|         nats_cmd.return_value = "ok" | ||||
|  | ||||
|         # setup data | ||||
|         policy = baker.make("automation.Policy", active=True) | ||||
|         tasks = baker.make( | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.3 on 2020-11-29 09:12 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('autotasks', '0008_auto_20201030_1515'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='run_time_bit_weekdays', | ||||
|             field=models.IntegerField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,33 @@ | ||||
| from django.db import migrations | ||||
| from tacticalrmm.utils import get_bit_days | ||||
|  | ||||
| DAYS_OF_WEEK = { | ||||
|     0: "Monday", | ||||
|     1: "Tuesday", | ||||
|     2: "Wednesday", | ||||
|     3: "Thursday", | ||||
|     4: "Friday", | ||||
|     5: "Saturday", | ||||
|     6: "Sunday", | ||||
| } | ||||
|  | ||||
|  | ||||
| def migrate_days(apps, schema_editor): | ||||
|     AutomatedTask = apps.get_model("autotasks", "AutomatedTask") | ||||
|     for task in AutomatedTask.objects.exclude(run_time_days__isnull=True).exclude( | ||||
|         run_time_days=[] | ||||
|     ): | ||||
|         run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days] | ||||
|         task.run_time_bit_weekdays = get_bit_days(run_days) | ||||
|         task.save(update_fields=["run_time_bit_weekdays"]) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("autotasks", "0009_automatedtask_run_time_bit_weekdays"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython(migrate_days), | ||||
|     ] | ||||
| @@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField | ||||
| from django.db.models.fields import DateTimeField | ||||
| from automation.models import Policy | ||||
| from logs.models import BaseAuditModel | ||||
| from tacticalrmm.utils import bitdays_to_string | ||||
|  | ||||
| RUN_TIME_DAY_CHOICES = [ | ||||
|     (0, "Monday"), | ||||
| @@ -69,6 +70,8 @@ class AutomatedTask(BaseAuditModel): | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     name = models.CharField(max_length=255) | ||||
|     run_time_bit_weekdays = models.IntegerField(null=True, blank=True) | ||||
|     # run_time_days is deprecated, use bit weekdays | ||||
|     run_time_days = ArrayField( | ||||
|         models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True), | ||||
|         null=True, | ||||
| @@ -107,21 +110,12 @@ class AutomatedTask(BaseAuditModel): | ||||
|         elif self.task_type == "runonce": | ||||
|             return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}' | ||||
|         elif self.task_type == "scheduled": | ||||
|             ret = [] | ||||
|             for i in self.run_time_days: | ||||
|                 for j in RUN_TIME_DAY_CHOICES: | ||||
|                     if i in j: | ||||
|                         ret.append(j[1][0:3]) | ||||
|  | ||||
|             run_time_nice = dt.datetime.strptime( | ||||
|                 self.run_time_minute, "%H:%M" | ||||
|             ).strftime("%I:%M %p") | ||||
|  | ||||
|             if len(ret) == 7: | ||||
|                 return f"Every day at {run_time_nice}" | ||||
|             else: | ||||
|                 days = ",".join(ret) | ||||
|                 return f"{days} at {run_time_nice}" | ||||
|             days = bitdays_to_string(self.run_time_bit_weekdays) | ||||
|             return f"{days} at {run_time_nice}" | ||||
|  | ||||
|     @property | ||||
|     def last_run_as_timezone(self): | ||||
| @@ -169,6 +163,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             name=self.name, | ||||
|             run_time_days=self.run_time_days, | ||||
|             run_time_minute=self.run_time_minute, | ||||
|             run_time_bit_weekdays=self.run_time_bit_weekdays, | ||||
|             run_time_date=self.run_time_date, | ||||
|             task_type=self.task_type, | ||||
|             win_task_name=self.win_task_name, | ||||
|   | ||||
| @@ -1,52 +1,37 @@ | ||||
| import asyncio | ||||
| import datetime as dt | ||||
| from loguru import logger | ||||
| from tacticalrmm.celery import app | ||||
| from django.conf import settings | ||||
| import pytz | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from .models import AutomatedTask | ||||
| from logs.models import PendingAction | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| DAYS_OF_WEEK = { | ||||
|     0: "Monday", | ||||
|     1: "Tuesday", | ||||
|     2: "Wednesday", | ||||
|     3: "Thursday", | ||||
|     4: "Friday", | ||||
|     5: "Saturday", | ||||
|     6: "Sunday", | ||||
| } | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def create_win_task_schedule(pk, pending_action=False): | ||||
|     task = AutomatedTask.objects.get(pk=pk) | ||||
|  | ||||
|     if task.task_type == "scheduled": | ||||
|         run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days] | ||||
|  | ||||
|         r = task.agent.salt_api_cmd( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task.win_task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {task.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Weekly", | ||||
|                 f'start_time="{task.run_time_minute}"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|             ], | ||||
|             kwargs={"days_of_week": run_days}, | ||||
|         ) | ||||
|         nats_data = { | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "rmm", | ||||
|                 "trigger": "weekly", | ||||
|                 "weekdays": task.run_time_bit_weekdays, | ||||
|                 "pk": task.pk, | ||||
|                 "name": task.win_task_name, | ||||
|                 "hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour, | ||||
|                 "min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute, | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|     elif task.task_type == "runonce": | ||||
|  | ||||
|         # check if scheduled time is in the past | ||||
|         agent_tz = pytz.timezone(task.agent.timezone) | ||||
|         task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc) | ||||
| @@ -57,45 +42,41 @@ def create_win_task_schedule(pk, pending_action=False): | ||||
|             ) + djangotime.timedelta(minutes=5) | ||||
|             task.save() | ||||
|  | ||||
|         r = task.agent.salt_api_cmd( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task.win_task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {task.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 f'start_date="{task.run_time_date.strftime("%Y-%m-%d")}"', | ||||
|                 f'start_time="{task.run_time_date.strftime("%H:%M")}"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|                 "start_when_available=True", | ||||
|             ], | ||||
|         ) | ||||
|         nats_data = { | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "rmm", | ||||
|                 "trigger": "once", | ||||
|                 "pk": task.pk, | ||||
|                 "name": task.win_task_name, | ||||
|                 "year": int(dt.datetime.strftime(task.run_time_date, "%Y")), | ||||
|                 "month": dt.datetime.strftime(task.run_time_date, "%B"), | ||||
|                 "day": int(dt.datetime.strftime(task.run_time_date, "%d")), | ||||
|                 "hour": int(dt.datetime.strftime(task.run_time_date, "%H")), | ||||
|                 "min": int(dt.datetime.strftime(task.run_time_date, "%M")), | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         if task.remove_if_not_scheduled and pyver.parse( | ||||
|             task.agent.version | ||||
|         ) >= pyver.parse("1.1.2"): | ||||
|             nats_data["schedtaskpayload"]["deleteafter"] = True | ||||
|  | ||||
|     elif task.task_type == "checkfailure" or task.task_type == "manual": | ||||
|         r = task.agent.salt_api_cmd( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task.win_task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {task.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 'start_date="1975-01-01"', | ||||
|                 'start_time="01:00"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|             ], | ||||
|         ) | ||||
|         nats_data = { | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "rmm", | ||||
|                 "trigger": "manual", | ||||
|                 "pk": task.pk, | ||||
|                 "name": task.win_task_name, | ||||
|             }, | ||||
|         } | ||||
|     else: | ||||
|         return "error" | ||||
|  | ||||
|     if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): | ||||
|     r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10)) | ||||
|     if r != "ok": | ||||
|         # don't create pending action if this task was initiated by a pending action | ||||
|         if not pending_action: | ||||
|             PendingAction( | ||||
| @@ -129,13 +110,16 @@ def create_win_task_schedule(pk, pending_action=False): | ||||
| def enable_or_disable_win_task(pk, action, pending_action=False): | ||||
|     task = AutomatedTask.objects.get(pk=pk) | ||||
|  | ||||
|     r = task.agent.salt_api_cmd( | ||||
|         timeout=20, | ||||
|         func="task.edit_task", | ||||
|         arg=[f"name={task.win_task_name}", f"enabled={action}"], | ||||
|     ) | ||||
|     nats_data = { | ||||
|         "func": "enableschedtask", | ||||
|         "schedtaskpayload": { | ||||
|             "name": task.win_task_name, | ||||
|             "enabled": action, | ||||
|         }, | ||||
|     } | ||||
|     r = asyncio.run(task.agent.nats_cmd(nats_data)) | ||||
|  | ||||
|     if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): | ||||
|     if r != "ok": | ||||
|         # don't create pending action if this task was initiated by a pending action | ||||
|         if not pending_action: | ||||
|             PendingAction( | ||||
| @@ -150,9 +134,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False): | ||||
|             task.sync_status = "notsynced" | ||||
|             task.save(update_fields=["sync_status"]) | ||||
|  | ||||
|         logger.error( | ||||
|             f"Unable to update the scheduled task {task.win_task_name} on {task.agent.hostname}. It will be updated when the agent checks in." | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     # clear pending action since it was successful | ||||
| @@ -163,7 +144,6 @@ def enable_or_disable_win_task(pk, action, pending_action=False): | ||||
|  | ||||
|     task.sync_status = "synced" | ||||
|     task.save(update_fields=["sync_status"]) | ||||
|     logger.info(f"{task.agent.hostname} task {task.name} was edited.") | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @@ -171,13 +151,13 @@ def enable_or_disable_win_task(pk, action, pending_action=False): | ||||
| def delete_win_task_schedule(pk, pending_action=False): | ||||
|     task = AutomatedTask.objects.get(pk=pk) | ||||
|  | ||||
|     r = task.agent.salt_api_cmd( | ||||
|         timeout=20, | ||||
|         func="task.delete_task", | ||||
|         arg=[f"name={task.win_task_name}"], | ||||
|     ) | ||||
|     nats_data = { | ||||
|         "func": "delschedtask", | ||||
|         "schedtaskpayload": {"name": task.win_task_name}, | ||||
|     } | ||||
|     r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10)) | ||||
|  | ||||
|     if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): | ||||
|     if r != "ok": | ||||
|         # don't create pending action if this task was initiated by a pending action | ||||
|         if not pending_action: | ||||
|             PendingAction( | ||||
| @@ -188,9 +168,6 @@ def delete_win_task_schedule(pk, pending_action=False): | ||||
|             task.sync_status = "pendingdeletion" | ||||
|             task.save(update_fields=["sync_status"]) | ||||
|  | ||||
|         logger.error( | ||||
|             f"Unable to delete scheduled task {task.win_task_name} on {task.agent.hostname}. It was marked pending deletion and will be removed when the agent checks in." | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     # complete pending action since it was successful | ||||
| @@ -200,15 +177,13 @@ def delete_win_task_schedule(pk, pending_action=False): | ||||
|         pendingaction.save(update_fields=["status"]) | ||||
|  | ||||
|     task.delete() | ||||
|     logger.info(f"{task.agent.hostname} task {task.name} was deleted.") | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def run_win_task(pk): | ||||
|     # TODO deprecated, remove this function once salt gone | ||||
|     task = AutomatedTask.objects.get(pk=pk) | ||||
|     r = task.agent.salt_api_async(func="task.run", arg=[f"name={task.win_task_name}"]) | ||||
|     asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False)) | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @@ -220,18 +195,9 @@ def remove_orphaned_win_tasks(agentpk): | ||||
|  | ||||
|     logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.") | ||||
|  | ||||
|     r = agent.salt_api_cmd( | ||||
|         timeout=15, | ||||
|         func="task.list_tasks", | ||||
|     ) | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10)) | ||||
|  | ||||
|     if r == "timeout" or r == "error": | ||||
|         logger.error( | ||||
|             f"Unable to clean up scheduled tasks on {agent.hostname}. Agent might be offline" | ||||
|         ) | ||||
|         return "errtimeout" | ||||
|  | ||||
|     if not isinstance(r, list): | ||||
|     if not isinstance(r, list) and not r:  # empty list | ||||
|         logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}") | ||||
|         return "notlist" | ||||
|  | ||||
| @@ -240,7 +206,8 @@ def remove_orphaned_win_tasks(agentpk): | ||||
|     exclude_tasks = ( | ||||
|         "TacticalRMM_fixmesh", | ||||
|         "TacticalRMM_SchedReboot", | ||||
|         "TacticalRMM_saltwatchdog",  # will be implemented in future | ||||
|         "TacticalRMM_sync", | ||||
|         "TacticalRMM_agentupdate", | ||||
|     ) | ||||
|  | ||||
|     for task in r: | ||||
| @@ -250,16 +217,16 @@ def remove_orphaned_win_tasks(agentpk): | ||||
|  | ||||
|         if task.startswith("TacticalRMM_") and task not in agent_task_names: | ||||
|             # delete task since it doesn't exist in UI | ||||
|             ret = agent.salt_api_cmd( | ||||
|                 timeout=20, | ||||
|                 func="task.delete_task", | ||||
|                 arg=[f"name={task}"], | ||||
|             ) | ||||
|             if isinstance(ret, bool) and ret is True: | ||||
|                 logger.info(f"Removed orphaned task {task} from {agent.hostname}") | ||||
|             else: | ||||
|             nats_data = { | ||||
|                 "func": "delschedtask", | ||||
|                 "schedtaskpayload": {"name": task}, | ||||
|             } | ||||
|             ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) | ||||
|             if ret != "ok": | ||||
|                 logger.error( | ||||
|                     f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}" | ||||
|                 ) | ||||
|             else: | ||||
|                 logger.info(f"Removed orphaned task {task} from {agent.hostname}") | ||||
|  | ||||
|     logger.info(f"Orphaned task cleanup finished on {agent.hostname}") | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import datetime as dt | ||||
| from unittest.mock import patch, call | ||||
| from model_bakery import baker | ||||
| from django.utils import timezone as djangotime | ||||
| @@ -25,9 +26,9 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         # setup data | ||||
|         script = baker.make_recipe("scripts.script") | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         agent_old = baker.make_recipe("agents.agent", version="0.9.0") | ||||
|         policy = baker.make("automation.Policy") | ||||
|         check = baker.make_recipe("checks.diskspace_check", agent=agent) | ||||
|         old_agent = baker.make_recipe("agents.agent", version="1.1.0") | ||||
|  | ||||
|         # test script set to invalid pk | ||||
|         data = {"autotask": {"script": 500}} | ||||
| @@ -50,10 +51,10 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         # test invalid agent version | ||||
|         # test old agent version | ||||
|         data = { | ||||
|             "autotask": {"script": script.id, "script_args": ["args"]}, | ||||
|             "agent": agent_old.id, | ||||
|             "autotask": {"script": script.id}, | ||||
|             "agent": old_agent.id, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, data, format="json") | ||||
| @@ -63,7 +64,7 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         data = { | ||||
|             "autotask": { | ||||
|                 "name": "Test Task Scheduled with Assigned Check", | ||||
|                 "run_time_days": [0, 1, 2], | ||||
|                 "run_time_days": ["Sunday", "Monday", "Friday"], | ||||
|                 "run_time_minute": "10:00", | ||||
|                 "timeout": 120, | ||||
|                 "enabled": True, | ||||
| @@ -84,6 +85,7 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         data = { | ||||
|             "autotask": { | ||||
|                 "name": "Test Task Manual", | ||||
|                 "run_time_days": [], | ||||
|                 "timeout": 120, | ||||
|                 "enabled": True, | ||||
|                 "script": script.id, | ||||
| @@ -213,8 +215,8 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_remove_orphaned_win_task(self, salt_api_cmd): | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_remove_orphaned_win_task(self, nats_cmd): | ||||
|         self.agent = baker.make_recipe("agents.agent") | ||||
|         self.task1 = AutomatedTask.objects.create( | ||||
|             agent=self.agent, | ||||
| @@ -222,20 +224,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             win_task_name=AutomatedTask.generate_task_name(), | ||||
|         ) | ||||
|  | ||||
|         salt_api_cmd.return_value = "timeout" | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         self.assertEqual(ret.result, "errtimeout") | ||||
|  | ||||
|         salt_api_cmd.return_value = "error" | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         self.assertEqual(ret.result, "errtimeout") | ||||
|  | ||||
|         salt_api_cmd.return_value = "task not found in" | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         self.assertEqual(ret.result, "notlist") | ||||
|  | ||||
|         salt_api_cmd.reset_mock() | ||||
|  | ||||
|         # test removing an orphaned task | ||||
|         win_tasks = [ | ||||
|             "Adobe Acrobat Update Task", | ||||
| @@ -250,50 +238,54 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|         ] | ||||
|  | ||||
|         self.calls = [ | ||||
|             call(timeout=15, func="task.list_tasks"), | ||||
|             call({"func": "listschedtasks"}, timeout=10), | ||||
|             call( | ||||
|                 timeout=20, | ||||
|                 func="task.delete_task", | ||||
|                 arg=["name=TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"], | ||||
|                 { | ||||
|                     "func": "delschedtask", | ||||
|                     "schedtaskpayload": { | ||||
|                         "name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb" | ||||
|                     }, | ||||
|                 }, | ||||
|                 timeout=10, | ||||
|             ), | ||||
|         ] | ||||
|  | ||||
|         salt_api_cmd.side_effect = [win_tasks, True] | ||||
|         nats_cmd.side_effect = [win_tasks, "ok"] | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         self.assertEqual(salt_api_cmd.call_count, 2) | ||||
|         salt_api_cmd.assert_has_calls(self.calls) | ||||
|         self.assertEqual(nats_cmd.call_count, 2) | ||||
|         nats_cmd.assert_has_calls(self.calls) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         # test salt delete_task fail | ||||
|         salt_api_cmd.reset_mock() | ||||
|         salt_api_cmd.side_effect = [win_tasks, False] | ||||
|         # test nats delete task fail | ||||
|         nats_cmd.reset_mock() | ||||
|         nats_cmd.side_effect = [win_tasks, "error deleting task"] | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         salt_api_cmd.assert_has_calls(self.calls) | ||||
|         self.assertEqual(salt_api_cmd.call_count, 2) | ||||
|         nats_cmd.assert_has_calls(self.calls) | ||||
|         self.assertEqual(nats_cmd.call_count, 2) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         # no orphaned tasks | ||||
|         salt_api_cmd.reset_mock() | ||||
|         nats_cmd.reset_mock() | ||||
|         win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb") | ||||
|         salt_api_cmd.side_effect = [win_tasks, True] | ||||
|         nats_cmd.side_effect = [win_tasks, "ok"] | ||||
|         ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() | ||||
|         self.assertEqual(salt_api_cmd.call_count, 1) | ||||
|         self.assertEqual(nats_cmd.call_count, 1) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_async") | ||||
|     def test_run_win_task(self, salt_api_async): | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_run_win_task(self, nats_cmd): | ||||
|         self.agent = baker.make_recipe("agents.agent") | ||||
|         self.task1 = AutomatedTask.objects.create( | ||||
|             agent=self.agent, | ||||
|             name="test task 1", | ||||
|             win_task_name=AutomatedTask.generate_task_name(), | ||||
|         ) | ||||
|         salt_api_async.return_value = "Response 200" | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = run_win_task.s(self.task1.pk).apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_create_win_task_schedule(self, salt_api_cmd): | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_create_win_task_schedule(self, nats_cmd): | ||||
|         self.agent = baker.make_recipe("agents.agent") | ||||
|  | ||||
|         task_name = AutomatedTask.generate_task_name() | ||||
| @@ -303,46 +295,32 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             name="test task 1", | ||||
|             win_task_name=task_name, | ||||
|             task_type="scheduled", | ||||
|             run_time_days=[0, 1, 6], | ||||
|             run_time_bit_weekdays=127, | ||||
|             run_time_minute="21:55", | ||||
|         ) | ||||
|         self.assertEqual(self.task1.sync_status, "notsynced") | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() | ||||
|         self.assertEqual(salt_api_cmd.call_count, 1) | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {self.task1.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Weekly", | ||||
|                 'start_time="21:55"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|             ], | ||||
|             kwargs={"days_of_week": ["Monday", "Tuesday", "Sunday"]}, | ||||
|         self.assertEqual(nats_cmd.call_count, 1) | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "schedtask", | ||||
|                 "schedtaskpayload": { | ||||
|                     "type": "rmm", | ||||
|                     "trigger": "weekly", | ||||
|                     "weekdays": 127, | ||||
|                     "pk": self.task1.pk, | ||||
|                     "name": task_name, | ||||
|                     "hour": 21, | ||||
|                     "min": 55, | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=10, | ||||
|         ) | ||||
|         self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) | ||||
|         self.assertEqual(self.task1.sync_status, "synced") | ||||
|  | ||||
|         salt_api_cmd.return_value = "timeout" | ||||
|         ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) | ||||
|         self.assertEqual(self.task1.sync_status, "notsynced") | ||||
|  | ||||
|         salt_api_cmd.return_value = "error" | ||||
|         ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) | ||||
|         self.assertEqual(self.task1.sync_status, "notsynced") | ||||
|  | ||||
|         salt_api_cmd.return_value = False | ||||
|         nats_cmd.return_value = "timeout" | ||||
|         ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply() | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|         self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) | ||||
| @@ -353,7 +331,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             agent=self.agent, action_type="taskaction" | ||||
|         ) | ||||
|         self.assertEqual(self.pending_action.status, "pending") | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s( | ||||
|             pk=self.task1.pk, pending_action=self.pending_action.pk | ||||
|         ).apply() | ||||
| @@ -362,7 +340,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|         self.assertEqual(self.pending_action.status, "completed") | ||||
|  | ||||
|         # test runonce with future date | ||||
|         salt_api_cmd.reset_mock() | ||||
|         nats_cmd.reset_mock() | ||||
|         task_name = AutomatedTask.generate_task_name() | ||||
|         run_time_date = djangotime.now() + djangotime.timedelta(hours=22) | ||||
|         self.task2 = AutomatedTask.objects.create( | ||||
| @@ -372,30 +350,29 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             task_type="runonce", | ||||
|             run_time_date=run_time_date, | ||||
|         ) | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply() | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {self.task2.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 f'start_date="{run_time_date.strftime("%Y-%m-%d")}"', | ||||
|                 f'start_time="{run_time_date.strftime("%H:%M")}"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|                 "start_when_available=True", | ||||
|             ], | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "schedtask", | ||||
|                 "schedtaskpayload": { | ||||
|                     "type": "rmm", | ||||
|                     "trigger": "once", | ||||
|                     "pk": self.task2.pk, | ||||
|                     "name": task_name, | ||||
|                     "year": int(dt.datetime.strftime(self.task2.run_time_date, "%Y")), | ||||
|                     "month": dt.datetime.strftime(self.task2.run_time_date, "%B"), | ||||
|                     "day": int(dt.datetime.strftime(self.task2.run_time_date, "%d")), | ||||
|                     "hour": int(dt.datetime.strftime(self.task2.run_time_date, "%H")), | ||||
|                     "min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")), | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=10, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         # test runonce with date in the past | ||||
|         salt_api_cmd.reset_mock() | ||||
|         nats_cmd.reset_mock() | ||||
|         task_name = AutomatedTask.generate_task_name() | ||||
|         run_time_date = djangotime.now() - djangotime.timedelta(days=13) | ||||
|         self.task3 = AutomatedTask.objects.create( | ||||
| @@ -405,31 +382,13 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             task_type="runonce", | ||||
|             run_time_date=run_time_date, | ||||
|         ) | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply() | ||||
|         self.task3 = AutomatedTask.objects.get(pk=self.task3.pk) | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {self.task3.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 f'start_date="{self.task3.run_time_date.strftime("%Y-%m-%d")}"', | ||||
|                 f'start_time="{self.task3.run_time_date.strftime("%H:%M")}"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|                 "start_when_available=True", | ||||
|             ], | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         # test checkfailure | ||||
|         salt_api_cmd.reset_mock() | ||||
|         nats_cmd.reset_mock() | ||||
|         self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent) | ||||
|         task_name = AutomatedTask.generate_task_name() | ||||
|         self.task4 = AutomatedTask.objects.create( | ||||
| @@ -439,29 +398,24 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             task_type="checkfailure", | ||||
|             assigned_check=self.check, | ||||
|         ) | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply() | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {self.task4.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 'start_date="1975-01-01"', | ||||
|                 'start_time="01:00"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|             ], | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "schedtask", | ||||
|                 "schedtaskpayload": { | ||||
|                     "type": "rmm", | ||||
|                     "trigger": "manual", | ||||
|                     "pk": self.task4.pk, | ||||
|                     "name": task_name, | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=10, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|  | ||||
|         # test manual | ||||
|         salt_api_cmd.reset_mock() | ||||
|         nats_cmd.reset_mock() | ||||
|         task_name = AutomatedTask.generate_task_name() | ||||
|         self.task5 = AutomatedTask.objects.create( | ||||
|             agent=self.agent, | ||||
| @@ -469,23 +423,18 @@ class TestAutoTaskCeleryTasks(TacticalTestCase): | ||||
|             win_task_name=task_name, | ||||
|             task_type="manual", | ||||
|         ) | ||||
|         salt_api_cmd.return_value = True | ||||
|         nats_cmd.return_value = "ok" | ||||
|         ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply() | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=20, | ||||
|             func="task.create_task", | ||||
|             arg=[ | ||||
|                 f"name={task_name}", | ||||
|                 "force=True", | ||||
|                 "action_type=Execute", | ||||
|                 'cmd="C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"', | ||||
|                 f'arguments="-m taskrunner -p {self.task5.pk}"', | ||||
|                 "start_in=C:\\Program Files\\TacticalAgent", | ||||
|                 "trigger_type=Once", | ||||
|                 'start_date="1975-01-01"', | ||||
|                 'start_time="01:00"', | ||||
|                 "ac_only=False", | ||||
|                 "stop_if_on_batteries=False", | ||||
|             ], | ||||
|         nats_cmd.assert_called_with( | ||||
|             { | ||||
|                 "func": "schedtask", | ||||
|                 "schedtaskpayload": { | ||||
|                     "type": "rmm", | ||||
|                     "trigger": "manual", | ||||
|                     "pk": self.task5.pk, | ||||
|                     "name": task_name, | ||||
|                 }, | ||||
|             }, | ||||
|             timeout=10, | ||||
|         ) | ||||
|         self.assertEqual(ret.status, "SUCCESS") | ||||
|   | ||||
| @@ -20,7 +20,7 @@ from .tasks import ( | ||||
|     delete_win_task_schedule, | ||||
|     enable_or_disable_win_task, | ||||
| ) | ||||
| from tacticalrmm.utils import notify_error | ||||
| from tacticalrmm.utils import notify_error, get_bit_days | ||||
|  | ||||
|  | ||||
| class AddAutoTask(APIView): | ||||
| @@ -38,17 +38,20 @@ class AddAutoTask(APIView): | ||||
|             parent = {"policy": policy} | ||||
|         else: | ||||
|             agent = get_object_or_404(Agent, pk=data["agent"]) | ||||
|             if not agent.has_gotasks: | ||||
|                 return notify_error("Requires agent version 1.1.1 or greater") | ||||
|  | ||||
|             parent = {"agent": agent} | ||||
|             added = "0.11.0" | ||||
|             if data["autotask"]["script_args"] and agent.not_supported(added): | ||||
|                 return notify_error( | ||||
|                     f"Script arguments only available in agent {added} or greater" | ||||
|                 ) | ||||
|  | ||||
|         check = None | ||||
|         if data["autotask"]["assigned_check"]: | ||||
|             check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"]) | ||||
|  | ||||
|         bit_weekdays = None | ||||
|         if data["autotask"]["run_time_days"]: | ||||
|             bit_weekdays = get_bit_days(data["autotask"]["run_time_days"]) | ||||
|  | ||||
|         del data["autotask"]["run_time_days"] | ||||
|         serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save( | ||||
| @@ -56,6 +59,7 @@ class AddAutoTask(APIView): | ||||
|             script=script, | ||||
|             win_task_name=AutomatedTask.generate_task_name(), | ||||
|             assigned_check=check, | ||||
|             run_time_bit_weekdays=bit_weekdays, | ||||
|         ) | ||||
|  | ||||
|         if not "policy" in data: | ||||
|   | ||||
| @@ -36,17 +36,6 @@ class AddCheck(APIView): | ||||
|         else: | ||||
|             agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|             parent = {"agent": agent} | ||||
|             added = "0.11.0" | ||||
|             if ( | ||||
|                 request.data["check"]["check_type"] == "script" | ||||
|                 and request.data["check"]["script_args"] | ||||
|                 and agent.not_supported(version_added=added) | ||||
|             ): | ||||
|                 return notify_error( | ||||
|                     { | ||||
|                         "non_field_errors": f"Script arguments only available in agent {added} or greater" | ||||
|                     } | ||||
|                 ) | ||||
|  | ||||
|         script = None | ||||
|         if "script" in request.data["check"]: | ||||
| @@ -58,13 +47,6 @@ class AddCheck(APIView): | ||||
|             request.data["check"]["check_type"] == "eventlog" | ||||
|             and request.data["check"]["event_id_is_wildcard"] | ||||
|         ): | ||||
|             if agent and agent.not_supported(version_added="0.10.2"): | ||||
|                 return notify_error( | ||||
|                     { | ||||
|                         "non_field_errors": "Wildcard is only available in agent 0.10.2 or greater" | ||||
|                     } | ||||
|                 ) | ||||
|  | ||||
|             request.data["check"]["event_id"] = 0 | ||||
|  | ||||
|         serializer = CheckSerializer( | ||||
| @@ -116,31 +98,8 @@ class GetUpdateDeleteCheck(APIView): | ||||
|                 pass | ||||
|             else: | ||||
|                 if request.data["event_id_is_wildcard"]: | ||||
|                     if check.agent.not_supported(version_added="0.10.2"): | ||||
|                         return notify_error( | ||||
|                             { | ||||
|                                 "non_field_errors": "Wildcard is only available in agent 0.10.2 or greater" | ||||
|                             } | ||||
|                         ) | ||||
|  | ||||
|                     request.data["event_id"] = 0 | ||||
|  | ||||
|         elif check.check_type == "script": | ||||
|             added = "0.11.0" | ||||
|             try: | ||||
|                 request.data["script_args"] | ||||
|             except KeyError: | ||||
|                 pass | ||||
|             else: | ||||
|                 if request.data["script_args"] and check.agent.not_supported( | ||||
|                     version_added=added | ||||
|                 ): | ||||
|                     return notify_error( | ||||
|                         { | ||||
|                             "non_field_errors": f"Script arguments only available in agent {added} or greater" | ||||
|                         } | ||||
|                     ) | ||||
|  | ||||
|         serializer = CheckSerializer(instance=check, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save() | ||||
|   | ||||
| @@ -38,7 +38,6 @@ class Client(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def has_failing_checks(self): | ||||
|  | ||||
|         agents = ( | ||||
|             Agent.objects.only( | ||||
|                 "pk", | ||||
| @@ -50,14 +49,17 @@ class Client(BaseAuditModel): | ||||
|             .filter(site__client=self) | ||||
|             .prefetch_related("agentchecks") | ||||
|         ) | ||||
|  | ||||
|         failing = 0 | ||||
|         for agent in agents: | ||||
|             if agent.checks["has_failing_checks"]: | ||||
|                 return True | ||||
|                 failing += 1 | ||||
|  | ||||
|             if agent.overdue_email_alert or agent.overdue_text_alert: | ||||
|                 return agent.status == "overdue" | ||||
|                 if agent.status == "overdue": | ||||
|                     failing += 1 | ||||
|  | ||||
|         return False | ||||
|         return failing > 0 | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(client): | ||||
| @@ -98,7 +100,6 @@ class Site(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def has_failing_checks(self): | ||||
|  | ||||
|         agents = ( | ||||
|             Agent.objects.only( | ||||
|                 "pk", | ||||
| @@ -110,14 +111,17 @@ class Site(BaseAuditModel): | ||||
|             .filter(site=self) | ||||
|             .prefetch_related("agentchecks") | ||||
|         ) | ||||
|  | ||||
|         failing = 0 | ||||
|         for agent in agents: | ||||
|             if agent.checks["has_failing_checks"]: | ||||
|                 return True | ||||
|                 failing += 1 | ||||
|  | ||||
|             if agent.overdue_email_alert or agent.overdue_text_alert: | ||||
|                 return agent.status == "overdue" | ||||
|                 if agent.status == "overdue": | ||||
|                     failing += 1 | ||||
|  | ||||
|         return False | ||||
|         return failing > 0 | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(site): | ||||
|   | ||||
| @@ -56,8 +56,9 @@ func downloadAgent(filepath string) (err error) { | ||||
| func main() { | ||||
|  | ||||
| 	debugLog := flag.String("log", "", "Verbose output") | ||||
| 	localSalt := flag.String("local-salt", "", "Use local salt minion") | ||||
| 	localMesh := flag.String("local-mesh", "", "Use local mesh agent") | ||||
| 	noSalt := flag.Bool("nosalt", false, "Does not install salt") | ||||
| 	silent := flag.Bool("silent", false, "Do not popup any message boxes during installation") | ||||
| 	cert := flag.String("cert", "", "Path to ca.pem") | ||||
| 	timeout := flag.String("timeout", "", "Timeout for subprocess calls") | ||||
| 	flag.Parse() | ||||
| @@ -78,35 +79,39 @@ func main() { | ||||
| 	} | ||||
|  | ||||
| 	if debug { | ||||
| 		cmdArgs = append(cmdArgs, "--log", "DEBUG") | ||||
| 		cmdArgs = append(cmdArgs, "-log", "debug") | ||||
| 	} | ||||
|  | ||||
| 	if len(strings.TrimSpace(*localSalt)) != 0 { | ||||
| 		cmdArgs = append(cmdArgs, "--local-salt", *localSalt) | ||||
| 	if *silent { | ||||
| 		cmdArgs = append(cmdArgs, "-silent") | ||||
| 	} | ||||
|  | ||||
| 	if *noSalt { | ||||
| 		cmdArgs = append(cmdArgs, "-nosalt") | ||||
| 	} | ||||
|  | ||||
| 	if len(strings.TrimSpace(*localMesh)) != 0 { | ||||
| 		cmdArgs = append(cmdArgs, "--local-mesh", *localMesh) | ||||
| 		cmdArgs = append(cmdArgs, "-local-mesh", *localMesh) | ||||
| 	} | ||||
|  | ||||
| 	if len(strings.TrimSpace(*cert)) != 0 { | ||||
| 		cmdArgs = append(cmdArgs, "--cert", *cert) | ||||
| 		cmdArgs = append(cmdArgs, "-cert", *cert) | ||||
| 	} | ||||
|  | ||||
| 	if len(strings.TrimSpace(*timeout)) != 0 { | ||||
| 		cmdArgs = append(cmdArgs, "--timeout", *timeout) | ||||
| 		cmdArgs = append(cmdArgs, "-timeout", *timeout) | ||||
| 	} | ||||
|  | ||||
| 	if Rdp == "1" { | ||||
| 		cmdArgs = append(cmdArgs, "--rdp") | ||||
| 		cmdArgs = append(cmdArgs, "-rdp") | ||||
| 	} | ||||
|  | ||||
| 	if Ping == "1" { | ||||
| 		cmdArgs = append(cmdArgs, "--ping") | ||||
| 		cmdArgs = append(cmdArgs, "-ping") | ||||
| 	} | ||||
|  | ||||
| 	if Power == "1" { | ||||
| 		cmdArgs = append(cmdArgs, "--power") | ||||
| 		cmdArgs = append(cmdArgs, "-power") | ||||
| 	} | ||||
|  | ||||
| 	if debug { | ||||
| @@ -133,7 +138,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	time.Sleep(20 * time.Second) | ||||
| 	time.Sleep(10 * time.Second) | ||||
|  | ||||
| 	fmt.Println("Installation starting.") | ||||
| 	cmd := exec.Command(tacrmm, cmdArgs...) | ||||
|   | ||||
| @@ -36,7 +36,7 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) { | ||||
|         Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output | ||||
|         Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait | ||||
|         write-host ('Extracting...') | ||||
|         Start-Sleep -s 20 | ||||
|         Start-Sleep -s 10 | ||||
|         Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait | ||||
|         exit 0 | ||||
|     } | ||||
|   | ||||
| @@ -11,12 +11,11 @@ class Command(BaseCommand): | ||||
|     help = "Sets up initial mesh central configuration" | ||||
|  | ||||
|     async def websocket_call(self, mesh_settings): | ||||
|         token = get_auth_token( | ||||
|             mesh_settings.mesh_username, mesh_settings.mesh_token | ||||
|         ) | ||||
|         token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) | ||||
|  | ||||
|         if settings.MESH_WS_URL: | ||||
|             uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" | ||||
|         if settings.DOCKER_BUILD: | ||||
|             site = mesh_settings.mesh_site.replace("https", "ws") | ||||
|             uri = f"{site}:443/control.ashx?auth={token}" | ||||
|         else: | ||||
|             site = mesh_settings.mesh_site.replace("https", "wss") | ||||
|             uri = f"{site}/control.ashx?auth={token}" | ||||
|   | ||||
| @@ -12,12 +12,11 @@ class Command(BaseCommand): | ||||
|  | ||||
|     async def websocket_call(self, mesh_settings): | ||||
|  | ||||
|         token = get_auth_token( | ||||
|             mesh_settings.mesh_username, mesh_settings.mesh_token | ||||
|         ) | ||||
|         token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) | ||||
|  | ||||
|         if settings.MESH_WS_URL: | ||||
|             uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" | ||||
|         if settings.DOCKER_BUILD: | ||||
|             site = mesh_settings.mesh_site.replace("https", "ws") | ||||
|             uri = f"{site}:443/control.ashx?auth={token}" | ||||
|         else: | ||||
|             site = mesh_settings.mesh_site.replace("https", "wss") | ||||
|             uri = f"{site}/control.ashx?auth={token}" | ||||
| @@ -52,11 +51,17 @@ class Command(BaseCommand): | ||||
|  | ||||
|         try: | ||||
|             # Check for Mesh Username | ||||
|             if not mesh_settings.mesh_username or settings.MESH_USERNAME != mesh_settings.mesh_username: | ||||
|             if ( | ||||
|                 not mesh_settings.mesh_username | ||||
|                 or settings.MESH_USERNAME != mesh_settings.mesh_username | ||||
|             ): | ||||
|                 mesh_settings.mesh_username = settings.MESH_USERNAME | ||||
|  | ||||
|             # Check for Mesh Site | ||||
|             if not mesh_settings.mesh_site or settings.MESH_SITE != mesh_settings.mesh_site: | ||||
|             if ( | ||||
|                 not mesh_settings.mesh_site | ||||
|                 or settings.MESH_SITE != mesh_settings.mesh_site | ||||
|             ): | ||||
|                 mesh_settings.mesh_site = settings.MESH_SITE | ||||
|  | ||||
|             # Check for Mesh Token | ||||
| @@ -75,7 +80,9 @@ class Command(BaseCommand): | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings)) | ||||
|             asyncio.get_event_loop().run_until_complete( | ||||
|                 self.websocket_call(mesh_settings) | ||||
|             ) | ||||
|             self.stdout.write("Initial Mesh Central setup complete") | ||||
|         except websockets.exceptions.ConnectionClosedError: | ||||
|             self.stdout.write( | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import os | ||||
| import shutil | ||||
| import subprocess | ||||
| import sys | ||||
| import tempfile | ||||
| from time import sleep | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| @@ -15,18 +13,6 @@ class Command(BaseCommand): | ||||
|     help = "Collection of tasks to run after updating the rmm, after migrations" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         if not os.path.exists("/usr/local/bin/goversioninfo"): | ||||
|             self.stdout.write(self.style.ERROR("*" * 100)) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR( | ||||
|                     "ERROR: New update script available. Delete this one and re-download." | ||||
|                 ) | ||||
|             ) | ||||
|             self.stdout.write("\n") | ||||
|             sys.exit(1) | ||||
|  | ||||
|         # 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 | ||||
|         # the following will convert dicts to lists for agent's still on the python agent | ||||
| @@ -43,88 +29,17 @@ class Command(BaseCommand): | ||||
|                     self.style.SUCCESS(f"Migrated disks on {agent.hostname}") | ||||
|                 ) | ||||
|  | ||||
|         # sync modules. split into chunks of 60 agents to not overload the salt master | ||||
|         agents = Agent.objects.all() | ||||
|         online = [i.salt_id for i in agents if i.status == "online"] | ||||
|  | ||||
|         chunks = (online[i : i + 60] for i in range(0, len(online), 60)) | ||||
|  | ||||
|         self.stdout.write(self.style.SUCCESS("Syncing agent modules...")) | ||||
|         for chunk in chunks: | ||||
|             r = Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules") | ||||
|             sleep(5) | ||||
|  | ||||
|         has_old_config = True | ||||
|         rmm_conf = "/etc/nginx/sites-available/rmm.conf" | ||||
|         if os.path.exists(rmm_conf): | ||||
|             with open(rmm_conf) as f: | ||||
|                 for line in f: | ||||
|                     if "location" and "builtin" in line: | ||||
|                         has_old_config = False | ||||
|                         break | ||||
|  | ||||
|         if has_old_config: | ||||
|             new_conf = """ | ||||
|             location /builtin/ { | ||||
|                 internal; | ||||
|                 add_header "Access-Control-Allow-Origin" "https://rmm.yourwebsite.com"; | ||||
|                 alias /srv/salt/scripts/; | ||||
|             } | ||||
|             """ | ||||
|  | ||||
|             after_this = """ | ||||
|             location /saltscripts/ { | ||||
|                 internal; | ||||
|                 add_header "Access-Control-Allow-Origin" "https://rmm.yourwebsite.com"; | ||||
|                 alias /srv/salt/scripts/userdefined/; | ||||
|             } | ||||
|             """ | ||||
|  | ||||
|             self.stdout.write(self.style.ERROR("*" * 100)) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR( | ||||
|                     "WARNING: A recent update requires you to manually edit your nginx config" | ||||
|                 ) | ||||
|             ) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR("Please add the following location block to ") | ||||
|                 + self.style.WARNING(rmm_conf) | ||||
|             ) | ||||
|             self.stdout.write(self.style.SUCCESS(new_conf)) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR( | ||||
|                     "You can paste the above right after the following block that's already in your nginx config:" | ||||
|                 ) | ||||
|             ) | ||||
|             self.stdout.write(after_this) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR( | ||||
|                     "Make sure to replace rmm.yourwebsite.com with your domain" | ||||
|                 ) | ||||
|             ) | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR("After editing, restart nginx with the command ") | ||||
|                 + self.style.WARNING("sudo systemctl restart nginx") | ||||
|             ) | ||||
|             self.stdout.write("\n") | ||||
|             self.stdout.write(self.style.ERROR("*" * 100)) | ||||
|             input("Press Enter to continue...") | ||||
|  | ||||
|         # install go | ||||
|         if not os.path.exists("/usr/local/rmmgo/"): | ||||
|             self.stdout.write(self.style.SUCCESS("Installing golang")) | ||||
|             subprocess.run("sudo mkdir -p /usr/local/rmmgo", shell=True) | ||||
|             tmpdir = tempfile.mkdtemp() | ||||
|             r = subprocess.run( | ||||
|                 f"wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P {tmpdir}", | ||||
|                 f"wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P {tmpdir}", | ||||
|                 shell=True, | ||||
|             ) | ||||
|  | ||||
|             gotar = os.path.join(tmpdir, "go1.15.linux-amd64.tar.gz") | ||||
|             gotar = os.path.join(tmpdir, "go1.15.5.linux-amd64.tar.gz") | ||||
|  | ||||
|             subprocess.run(f"tar -xzf {gotar} -C {tmpdir}", shell=True) | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from core.tasks import core_maintenance_tasks | ||||
| from unittest.mock import patch | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
|  | ||||
| class TestCoreTasks(TacticalTestCase): | ||||
| @@ -31,3 +33,45 @@ class TestCoreTasks(TacticalTestCase): | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("autotasks.tasks.remove_orphaned_win_tasks.delay") | ||||
|     def test_ui_maintenance_actions(self, remove_orphaned_win_tasks): | ||||
|         url = "/core/servermaintenance/" | ||||
|  | ||||
|         agents = baker.make_recipe("agents.online_agent", _quantity=3) | ||||
|  | ||||
|         # test with empty data | ||||
|         r = self.client.post(url, {}) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         # test with invalid action | ||||
|         data = {"action": "invalid_action"} | ||||
|  | ||||
|         r = self.client.post(url, data) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         # test reload nats action | ||||
|         data = {"action": "reload_nats"} | ||||
|         r = self.client.post(url, data) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         # test prune db with no tables | ||||
|         data = {"action": "prune_db"} | ||||
|         r = self.client.post(url, data) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         # test prune db with tables | ||||
|         data = { | ||||
|             "action": "prune_db", | ||||
|             "prune_tables": ["audit_logs", "agent_outages", "pending_actions"], | ||||
|         } | ||||
|         r = self.client.post(url, data) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         # test remove orphaned tasks | ||||
|         data = {"action": "rm_orphaned_tasks"} | ||||
|         r = self.client.post(url, data) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         remove_orphaned_win_tasks.assert_called() | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|   | ||||
| @@ -8,4 +8,5 @@ urlpatterns = [ | ||||
|     path("version/", views.version), | ||||
|     path("emailtest/", views.email_test), | ||||
|     path("dashinfo/", views.dashboard_info), | ||||
|     path("servermaintenance/", views.server_maintenance), | ||||
| ] | ||||
|   | ||||
| @@ -84,3 +84,56 @@ def email_test(request): | ||||
|         return notify_error(r) | ||||
|  | ||||
|     return Response("Email Test OK!") | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def server_maintenance(request): | ||||
|     from tacticalrmm.utils import reload_nats | ||||
|  | ||||
|     if "action" not in request.data: | ||||
|         return notify_error("The data is incorrect") | ||||
|  | ||||
|     if request.data["action"] == "reload_nats": | ||||
|         reload_nats() | ||||
|         return Response("Nats configuration was reloaded successfully.") | ||||
|  | ||||
|     if request.data["action"] == "rm_orphaned_tasks": | ||||
|         from agents.models import Agent | ||||
|         from autotasks.tasks import remove_orphaned_win_tasks | ||||
|  | ||||
|         agents = Agent.objects.all() | ||||
|         online = [i for i in agents if i.status == "online"] | ||||
|         for agent in online: | ||||
|             remove_orphaned_win_tasks.delay(agent.pk) | ||||
|  | ||||
|         return Response( | ||||
|             "The task has been initiated. Check the Debug Log in the UI for progress." | ||||
|         ) | ||||
|  | ||||
|     if request.data["action"] == "prune_db": | ||||
|         from agents.models import AgentOutage | ||||
|         from logs.models import AuditLog, PendingAction | ||||
|  | ||||
|         if "prune_tables" not in request.data: | ||||
|             return notify_error("The data is incorrect.") | ||||
|  | ||||
|         tables = request.data["prune_tables"] | ||||
|         records_count = 0 | ||||
|         if "agent_outages" in tables: | ||||
|             agentoutages = AgentOutage.objects.exclude(recovery_time=None) | ||||
|             records_count += agentoutages.count() | ||||
|             agentoutages.delete() | ||||
|  | ||||
|         if "audit_logs" in tables: | ||||
|             auditlogs = AuditLog.objects.filter(action="check_run") | ||||
|             records_count += auditlogs.count() | ||||
|             auditlogs.delete() | ||||
|  | ||||
|         if "pending_actions" in tables: | ||||
|             pendingactions = PendingAction.objects.filter(status="completed") | ||||
|             records_count += pendingactions.count() | ||||
|             pendingactions.delete() | ||||
|  | ||||
|         return Response(f"{records_count} records were pruned from the database") | ||||
|  | ||||
|     return notify_error("The data is incorrect") | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import datetime as dt | ||||
| import json | ||||
| from abc import abstractmethod | ||||
| from django.db import models | ||||
| from tacticalrmm.middleware import get_username, get_debug_info | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| from loguru import logger | ||||
| from tacticalrmm.celery import app | ||||
| from django.conf import settings | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def cancel_pending_action_task(data): | ||||
|  | ||||
|     if data["action_type"] == "schedreboot" and data["status"] == "pending": | ||||
|  | ||||
|         from agents.models import Agent | ||||
|  | ||||
|         agent = Agent.objects.get(pk=data["agent"]) | ||||
|  | ||||
|         task_name = data["details"]["taskname"] | ||||
|         r = agent.salt_api_cmd( | ||||
|             timeout=30, func="task.delete_task", arg=[f"name={task_name}"] | ||||
|         ) | ||||
|         if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): | ||||
|             logger.error( | ||||
|                 f"Unable to contact {agent.hostname}. Task {task_name} will need to cancelled manually." | ||||
|             ) | ||||
|             return | ||||
|         else: | ||||
|             logger.info(f"Scheduled reboot cancelled on {agent.hostname}") | ||||
|  | ||||
|     return "ok" | ||||
| @@ -122,10 +122,25 @@ class TestAuditViews(TacticalTestCase): | ||||
|             {"filter": {"clientFilter": [site.client.id]}, "count": 23}, | ||||
|         ] | ||||
|  | ||||
|         pagination = { | ||||
|             "rowsPerPage": 25, | ||||
|             "page": 1, | ||||
|             "sortBy": "entry_time", | ||||
|             "descending": True, | ||||
|         } | ||||
|  | ||||
|         for req in data: | ||||
|             resp = self.client.patch(url, req["filter"], format="json") | ||||
|             resp = self.client.patch( | ||||
|                 url, {**req["filter"], "pagination": pagination}, format="json" | ||||
|             ) | ||||
|             self.assertEqual(resp.status_code, 200) | ||||
|             self.assertEqual(len(resp.data), req["count"]) | ||||
|             self.assertEqual( | ||||
|                 len(resp.data["audit_logs"]), | ||||
|                 pagination["rowsPerPage"] | ||||
|                 if req["count"] > pagination["rowsPerPage"] | ||||
|                 else req["count"], | ||||
|             ) | ||||
|             self.assertEqual(resp.data["total"], req["count"]) | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
| @@ -190,54 +205,31 @@ class TestAuditViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("logs.tasks.cancel_pending_action_task.delay") | ||||
|     def test_cancel_pending_action(self, mock_task): | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_cancel_pending_action(self, nats_cmd): | ||||
|         url = "/logs/cancelpendingaction/" | ||||
|         pending_action = baker.make("logs.PendingAction") | ||||
|         # 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( | ||||
|             "logs.PendingAction", | ||||
|             agent=agent, | ||||
|             details={ | ||||
|                 "time": "2021-01-13 18:20:00", | ||||
|                 "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         serializer = PendingActionSerializer(pending_action).data | ||||
|         data = {"pk": pending_action.id} | ||||
|         resp = self.client.delete(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         mock_task.assert_called_with(serializer) | ||||
|         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) | ||||
|         self.assertEqual(resp.status_code, 404) """ | ||||
|  | ||||
|         self.check_not_authenticated("delete", url) | ||||
|  | ||||
|  | ||||
| class TestLogsTasks(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_cancel_pending_action_task(self, mock_salt_cmd): | ||||
|         from .tasks import cancel_pending_action_task | ||||
|  | ||||
|         pending_action = baker.make( | ||||
|             "logs.PendingAction", | ||||
|             action_type="schedreboot", | ||||
|             status="pending", | ||||
|             details={"taskname": "test_name"}, | ||||
|         ) | ||||
|  | ||||
|         # data that is passed to the task | ||||
|         data = PendingActionSerializer(pending_action).data | ||||
|  | ||||
|         # set return value on mock to success | ||||
|         mock_salt_cmd.return_value = "success" | ||||
|         # call task with valid data and see if salt is called with correct data | ||||
|         ret = cancel_pending_action_task(data) | ||||
|         mock_salt_cmd.assert_called_with( | ||||
|             timeout=30, func="task.delete_task", arg=["name=test_name"] | ||||
|         ) | ||||
|         # this should return successful | ||||
|         self.assertEquals(ret, "ok") | ||||
|  | ||||
|         # this run should return false | ||||
|         mock_salt_cmd.reset_mock() | ||||
|         mock_salt_cmd.return_value = "timeout" | ||||
|         ret = cancel_pending_action_task(data) | ||||
|         self.assertEquals(ret, None) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import asyncio | ||||
| import subprocess | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -5,6 +6,7 @@ from django.shortcuts import get_object_or_404 | ||||
| from django.http import HttpResponse | ||||
| from django.utils import timezone as djangotime | ||||
| from django.db.models import Q | ||||
| from django.core.paginator import Paginator | ||||
| from datetime import datetime as dt | ||||
|  | ||||
| from rest_framework.response import Response | ||||
| @@ -18,7 +20,7 @@ from accounts.models import User | ||||
| from .serializers import PendingActionSerializer, AuditLogSerializer | ||||
| from agents.serializers import AgentHostnameSerializer | ||||
| from accounts.serializers import UserSerializer | ||||
| from .tasks import cancel_pending_action_task | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
|  | ||||
| class GetAuditLogs(APIView): | ||||
| @@ -26,6 +28,14 @@ class GetAuditLogs(APIView): | ||||
|         from clients.models import Client | ||||
|         from agents.models import Agent | ||||
|  | ||||
|         pagination = request.data["pagination"] | ||||
|  | ||||
|         order_by = ( | ||||
|             f"-{pagination['sortBy']}" | ||||
|             if pagination["descending"] | ||||
|             else f"{pagination['sortBy']}" | ||||
|         ) | ||||
|  | ||||
|         agentFilter = Q() | ||||
|         clientFilter = Q() | ||||
|         actionFilter = Q() | ||||
| @@ -67,9 +77,18 @@ class GetAuditLogs(APIView): | ||||
|             .filter(actionFilter) | ||||
|             .filter(objectFilter) | ||||
|             .filter(timeFilter) | ||||
|         ) | ||||
|         ).order_by(order_by) | ||||
|  | ||||
|         return Response(AuditLogSerializer(audit_logs, many=True).data) | ||||
|         paginator = Paginator(audit_logs, pagination["rowsPerPage"]) | ||||
|  | ||||
|         return Response( | ||||
|             { | ||||
|                 "audit_logs": AuditLogSerializer( | ||||
|                     paginator.get_page(pagination["page"]), many=True | ||||
|                 ).data, | ||||
|                 "total": paginator.count, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class FilterOptionsAuditLog(APIView): | ||||
| @@ -95,19 +114,26 @@ def agent_pending_actions(request, pk): | ||||
|  | ||||
| @api_view() | ||||
| def all_pending_actions(request): | ||||
|     actions = PendingAction.objects.all() | ||||
|     actions = PendingAction.objects.all().select_related("agent") | ||||
|     return Response(PendingActionSerializer(actions, many=True).data) | ||||
|  | ||||
|  | ||||
| @api_view(["DELETE"]) | ||||
| def cancel_pending_action(request): | ||||
|     action = get_object_or_404(PendingAction, pk=request.data["pk"]) | ||||
|     data = PendingActionSerializer(action).data | ||||
|     cancel_pending_action_task.delay(data) | ||||
|     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} will be cancelled shortly" | ||||
|     ) | ||||
|     return Response(f"{action.agent.hostname}: {action.description} was cancelled") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| amqp==2.6.1 | ||||
| asgiref==3.3.0 | ||||
| asgiref==3.3.1 | ||||
| asyncio-nats-client==0.11.4 | ||||
| billiard==3.6.3.0 | ||||
| celery==4.4.6 | ||||
| certifi==2020.11.8 | ||||
| certifi==2020.12.5 | ||||
| cffi==1.14.3 | ||||
| chardet==3.0.4 | ||||
| cryptography==3.2.1 | ||||
| decorator==4.4.2 | ||||
| Django==3.1.3 | ||||
| Django==3.1.4 | ||||
| django-cors-headers==3.5.0 | ||||
| django-rest-knox==4.1.0 | ||||
| djangorestframework==3.12.2 | ||||
| @@ -26,12 +26,11 @@ pyparsing==2.4.7 | ||||
| pytz==2020.4 | ||||
| qrcode==6.1 | ||||
| redis==3.5.3 | ||||
| requests==2.24.0 | ||||
| requests==2.25.0 | ||||
| six==1.15.0 | ||||
| sqlparse==0.4.1 | ||||
| tldextract==3.0.2 | ||||
| twilio==6.47.0 | ||||
| urllib3==1.25.11 | ||||
| twilio==6.49.0 | ||||
| urllib3==1.26.2 | ||||
| uWSGI==2.0.19.1 | ||||
| validators==0.18.1 | ||||
| vine==1.3.0 | ||||
|   | ||||
| @@ -9,21 +9,6 @@ class TestServiceViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|  | ||||
|     def test_get_services(self): | ||||
|  | ||||
|         # test a call where agent doesn't exist | ||||
|         resp = self.client.get("/services/500/services/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         agent = baker.make_recipe("agents.agent_with_services") | ||||
|         url = f"/services/{agent.pk}/services/" | ||||
|         serializer = ServicesSerializer(agent) | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(serializer.data, resp.data) | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_default_services(self): | ||||
|         url = "/services/defaultservices/" | ||||
|         resp = self.client.get(url, format="json") | ||||
| @@ -33,13 +18,13 @@ class TestServiceViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_get_refreshed_services(self, nats_cmd): | ||||
|     def test_get_services(self, nats_cmd): | ||||
|         # test a call where agent doesn't exist | ||||
|         resp = self.client.get("/services/500/refreshedservices/", format="json") | ||||
|         resp = self.client.get("/services/500/services/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         agent = baker.make_recipe("agents.agent_with_services") | ||||
|         url = f"/services/{agent.pk}/refreshedservices/" | ||||
|         url = f"/services/{agent.pk}/services/" | ||||
|  | ||||
|         nats_return = [ | ||||
|             { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ from . import views | ||||
| urlpatterns = [ | ||||
|     path("<int:pk>/services/", views.get_services), | ||||
|     path("defaultservices/", views.default_services), | ||||
|     path("<int:pk>/refreshedservices/", views.get_refreshed_services), | ||||
|     path("serviceaction/", views.service_action), | ||||
|     path("<int:pk>/<svcname>/servicedetail/", views.service_detail), | ||||
|     path("editservice/", views.edit_service), | ||||
|   | ||||
| @@ -19,17 +19,6 @@ logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| @api_view() | ||||
| def get_services(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     return Response(ServicesSerializer(agent).data) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| def default_services(request): | ||||
|     return Response(Check.load_default_services()) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| def get_refreshed_services(request, pk): | ||||
|     agent = get_object_or_404(Agent, pk=pk) | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
| @@ -43,6 +32,11 @@ def get_refreshed_services(request, pk): | ||||
|     return Response(ServicesSerializer(agent).data) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| def default_services(request): | ||||
|     return Response(Check.load_default_services()) | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| def service_action(request): | ||||
|     agent = get_object_or_404(Agent, pk=request.data["pk"]) | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import asyncio | ||||
| import string | ||||
| from time import sleep | ||||
| from loguru import logger | ||||
| from tacticalrmm.celery import app | ||||
| @@ -8,6 +7,7 @@ from django.utils import timezone as djangotime | ||||
|  | ||||
| from agents.models import Agent | ||||
| from .models import ChocoSoftware, ChocoLog, InstalledSoftware | ||||
| from tacticalrmm.utils import filter_software | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| @@ -87,44 +87,6 @@ def update_chocos(): | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def get_installed_software(pk): | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     if not agent.has_nats: | ||||
|         logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0") | ||||
|         return | ||||
|  | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=20)) | ||||
|     if r == "timeout" or r == "natsdown": | ||||
|         logger.error(f"{agent.salt_id} {r}") | ||||
|         return | ||||
|  | ||||
|     printable = set(string.printable) | ||||
|     sw = [] | ||||
|     for s in r: | ||||
|         sw.append( | ||||
|             { | ||||
|                 "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"], | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|         InstalledSoftware(agent=agent, software=sw).save() | ||||
|     else: | ||||
|         s = agent.installedsoftware_set.first() | ||||
|         s.software = sw | ||||
|         s.save(update_fields=["software"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def install_program(pk, name, version): | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
| @@ -169,6 +131,4 @@ def install_program(pk, name, version): | ||||
|         agent=agent, name=name, version=version, message=output, installed=installed | ||||
|     ).save() | ||||
|  | ||||
|     get_installed_software.delay(agent.pk) | ||||
|  | ||||
|     return "ok" | ||||
|   | ||||
| @@ -120,61 +120,8 @@ 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.nats_cmd") | ||||
|     def test_get_installed_software(self, nats_cmd): | ||||
|         from .tasks import get_installed_software | ||||
|  | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|  | ||||
|         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", | ||||
|             }, | ||||
|         ] | ||||
|  | ||||
|         # 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=20) | ||||
|         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=20) | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     @patch("software.tasks.get_installed_software.delay") | ||||
|     def test_install_program(self, get_installed_software, salt_api_cmd): | ||||
|     def test_install_program(self, salt_api_cmd): | ||||
|         from .tasks import install_program | ||||
|  | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
| @@ -195,6 +142,5 @@ class TestSoftwareTasks(TacticalTestCase): | ||||
|         salt_api_cmd.assert_called_with( | ||||
|             timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"] | ||||
|         ) | ||||
|         get_installed_software.assert_called_with(agent.pk) | ||||
|  | ||||
|         self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists()) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import asyncio | ||||
| import string | ||||
| from typing import Any | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
|  | ||||
| @@ -10,7 +10,7 @@ from agents.models import Agent | ||||
| from .models import ChocoSoftware, InstalledSoftware | ||||
| from .serializers import InstalledSoftwareSerializer | ||||
| from .tasks import install_program | ||||
| from tacticalrmm.utils import notify_error | ||||
| from tacticalrmm.utils import notify_error, filter_software | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @@ -45,25 +45,11 @@ def refresh_installed(request, pk): | ||||
|     if not agent.has_nats: | ||||
|         return notify_error("Requires agent version 1.1.0 or greater") | ||||
|  | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) | ||||
|     r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) | ||||
|     if r == "timeout" or r == "natsdown": | ||||
|         return notify_error("Unable to contact the agent") | ||||
|  | ||||
|     printable = set(string.printable) | ||||
|     sw = [] | ||||
|     for s in r: | ||||
|         sw.append( | ||||
|             { | ||||
|                 "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"], | ||||
|             } | ||||
|         ) | ||||
|     sw = filter_software(r) | ||||
|  | ||||
|     if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|         InstalledSoftware(agent=agent, software=sw).save() | ||||
|   | ||||
| @@ -37,18 +37,14 @@ app.conf.beat_schedule = { | ||||
|         "task": "agents.tasks.batch_sync_modules_task", | ||||
|         "schedule": crontab(minute=25, hour="*/4"), | ||||
|     }, | ||||
|     "sys-info": { | ||||
|         "task": "agents.tasks.batch_sysinfo_task", | ||||
|         "schedule": crontab(minute=15, hour="*/2"), | ||||
|     }, | ||||
|     "update-salt": { | ||||
|         "task": "agents.tasks.update_salt_minion_task", | ||||
|         "schedule": crontab(minute=20, hour="*/6"), | ||||
|     }, | ||||
|     "agent-auto-update": { | ||||
|         "task": "agents.tasks.auto_self_agent_update_task", | ||||
|         "schedule": crontab(minute=35, hour="*"), | ||||
|     }, | ||||
|     "agents-sync": { | ||||
|         "task": "agents.tasks.sync_sysinfo_task", | ||||
|         "schedule": crontab(minute=55, hour="*"), | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -16,16 +16,15 @@ def get_debug_info(): | ||||
| EXCLUDE_PATHS = ( | ||||
|     "/api/v3", | ||||
|     "/api/v2", | ||||
|     "/api/v1", | ||||
|     "/logs/auditlogs", | ||||
|     "/winupdate/winupdater", | ||||
|     "/winupdate/results", | ||||
|     f"/{settings.ADMIN_URL}", | ||||
|     "/logout", | ||||
|     "/agents/installagent", | ||||
|     "/logs/downloadlog", | ||||
| ) | ||||
|  | ||||
| ENDS_WITH = "/services/" | ||||
|  | ||||
|  | ||||
| class AuditMiddleware: | ||||
|     def __init__(self, get_response): | ||||
| @@ -37,7 +36,9 @@ class AuditMiddleware: | ||||
|         return response | ||||
|  | ||||
|     def process_view(self, request, view_func, view_args, view_kwargs): | ||||
|         if not request.path.startswith(EXCLUDE_PATHS): | ||||
|         if not request.path.startswith(EXCLUDE_PATHS) and not request.path.endswith( | ||||
|             ENDS_WITH | ||||
|         ): | ||||
|             # https://stackoverflow.com/questions/26240832/django-and-middleware-which-uses-request-user-is-always-anonymous | ||||
|             try: | ||||
|                 # DRF saves the class of the view function as the .cls property | ||||
|   | ||||
| @@ -15,25 +15,25 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe") | ||||
| AUTH_USER_MODEL = "accounts.User" | ||||
|  | ||||
| # latest release | ||||
| TRMM_VERSION = "0.2.0" | ||||
| TRMM_VERSION = "0.2.15" | ||||
|  | ||||
| # bump this version everytime vue code is changed | ||||
| # to alert user they need to manually refresh their browser | ||||
| APP_VER = "0.0.91" | ||||
| APP_VER = "0.0.99" | ||||
|  | ||||
| # https://github.com/wh1te909/salt | ||||
| LATEST_SALT_VER = "1.1.0" | ||||
|  | ||||
| # https://github.com/wh1te909/rmmagent | ||||
| LATEST_AGENT_VER = "1.1.0" | ||||
| LATEST_AGENT_VER = "1.1.10" | ||||
|  | ||||
| MESH_VER = "0.6.84" | ||||
| MESH_VER = "0.7.14" | ||||
|  | ||||
| SALT_MASTER_VER = "3002.2" | ||||
|  | ||||
| # for the update script, bump when need to recreate venv or npm install | ||||
| PIP_VER = "3" | ||||
| NPM_VER = "2" | ||||
| PIP_VER = "4" | ||||
| NPM_VER = "3" | ||||
|  | ||||
| DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe" | ||||
| DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe" | ||||
| @@ -58,7 +58,6 @@ INSTALLED_APPS = [ | ||||
|     "knox", | ||||
|     "corsheaders", | ||||
|     "accounts", | ||||
|     "api", | ||||
|     "apiv2", | ||||
|     "apiv3", | ||||
|     "clients", | ||||
| @@ -123,9 +122,15 @@ AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", | ||||
|     }, | ||||
|     {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, | ||||
|     {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, | ||||
|     {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", | ||||
|     }, | ||||
| ] | ||||
|  | ||||
|  | ||||
| @@ -150,39 +155,6 @@ LOG_CONFIG = { | ||||
|     "handlers": [{"sink": os.path.join(LOG_DIR, "debug.log"), "serialize": False}] | ||||
| } | ||||
|  | ||||
| if "TRAVIS" in os.environ: | ||||
|     DATABASES = { | ||||
|         "default": { | ||||
|             "ENGINE": "django.db.backends.postgresql", | ||||
|             "NAME": "travisci", | ||||
|             "USER": "travisci", | ||||
|             "PASSWORD": "travisSuperSekret6645", | ||||
|             "HOST": "127.0.0.1", | ||||
|             "PORT": "", | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     REST_FRAMEWORK = { | ||||
|         "DATETIME_FORMAT": "%b-%d-%Y - %H:%M", | ||||
|         "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), | ||||
|         "DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",), | ||||
|         "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), | ||||
|     } | ||||
|  | ||||
|     DEBUG = True | ||||
|     SECRET_KEY = "abcdefghijklmnoptravis123456789" | ||||
|  | ||||
|     ADMIN_URL = "abc123456/" | ||||
|  | ||||
|     SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts") | ||||
|     SALT_USERNAME = "travis" | ||||
|     SALT_PASSWORD = "travis" | ||||
|     MESH_USERNAME = "travis" | ||||
|     MESH_SITE = "https://example.com" | ||||
|     MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c" | ||||
|     REDIS_HOST = "localhost" | ||||
|     SALT_HOST = "127.0.0.1" | ||||
|  | ||||
| if "AZPIPELINE" in os.environ: | ||||
|     DATABASES = { | ||||
|         "default": { | ||||
| @@ -202,6 +174,8 @@ if "AZPIPELINE" in os.environ: | ||||
|         "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), | ||||
|     } | ||||
|  | ||||
|     ALLOWED_HOSTS = ["api.example.com"] | ||||
|     DOCKER_BUILD = True | ||||
|     DEBUG = True | ||||
|     SECRET_KEY = "abcdefghijklmnoptravis123456789" | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,6 @@ urlpatterns = [ | ||||
|     path("login/", LoginView.as_view()), | ||||
|     path("logout/", knox_views.LogoutView.as_view()), | ||||
|     path("logoutall/", knox_views.LogoutAllView.as_view()), | ||||
|     path("api/v1/", include("api.urls")), | ||||
|     path("api/v2/", include("apiv2.urls")), | ||||
|     path("api/v3/", include("apiv3.urls")), | ||||
|     path("clients/", include("clients.urls")), | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| import json | ||||
| import os | ||||
| import string | ||||
| import subprocess | ||||
| import tldextract | ||||
| import time | ||||
| from typing import List, Dict | ||||
| from loguru import logger | ||||
|  | ||||
| from django.conf import settings | ||||
| from rest_framework import status | ||||
| @@ -9,26 +12,102 @@ from rest_framework.response import Response | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
| SoftwareList = List[Dict[str, str]] | ||||
|  | ||||
| WEEK_DAYS = { | ||||
|     "Sunday": 0x1, | ||||
|     "Monday": 0x2, | ||||
|     "Tuesday": 0x4, | ||||
|     "Wednesday": 0x8, | ||||
|     "Thursday": 0x10, | ||||
|     "Friday": 0x20, | ||||
|     "Saturday": 0x40, | ||||
| } | ||||
|  | ||||
|  | ||||
| def get_bit_days(days: List[str]) -> int: | ||||
|     bit_days = 0 | ||||
|     for day in days: | ||||
|         bit_days |= WEEK_DAYS.get(day) | ||||
|     return bit_days | ||||
|  | ||||
|  | ||||
| def bitdays_to_string(day: int) -> str: | ||||
|     ret = [] | ||||
|     if day == 127: | ||||
|         return "Every day" | ||||
|  | ||||
|     if day & WEEK_DAYS["Sunday"]: | ||||
|         ret.append("Sunday") | ||||
|     if day & WEEK_DAYS["Monday"]: | ||||
|         ret.append("Monday") | ||||
|     if day & WEEK_DAYS["Tuesday"]: | ||||
|         ret.append("Tuesday") | ||||
|     if day & WEEK_DAYS["Wednesday"]: | ||||
|         ret.append("Wednesday") | ||||
|     if day & WEEK_DAYS["Thursday"]: | ||||
|         ret.append("Thursday") | ||||
|     if day & WEEK_DAYS["Friday"]: | ||||
|         ret.append("Friday") | ||||
|     if day & WEEK_DAYS["Saturday"]: | ||||
|         ret.append("Saturday") | ||||
|  | ||||
|     return ", ".join(ret) | ||||
|  | ||||
|  | ||||
| def filter_software(sw: SoftwareList) -> SoftwareList: | ||||
|     ret: SoftwareList = [] | ||||
|     printable = set(string.printable) | ||||
|     for s in sw: | ||||
|         ret.append( | ||||
|             { | ||||
|                 "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"], | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def reload_nats(): | ||||
|     users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] | ||||
|     agents = Agent.objects.prefetch_related("user").only("pk", "agent_id") | ||||
|     for agent in agents: | ||||
|         users.append({"user": agent.agent_id, "password": agent.user.auth_token.key}) | ||||
|         try: | ||||
|             users.append( | ||||
|                 {"user": agent.agent_id, "password": agent.user.auth_token.key} | ||||
|             ) | ||||
|         except: | ||||
|             logger.critical( | ||||
|                 f"{agent.hostname} does not have a user account, NATS will not work" | ||||
|             ) | ||||
|  | ||||
|     if not settings.DOCKER_BUILD: | ||||
|         tld = tldextract.extract(settings.ALLOWED_HOSTS[0]) | ||||
|         domain = tld.domain + "." + tld.suffix | ||||
|         cert_path = f"/etc/letsencrypt/live/{domain}" | ||||
|     domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] | ||||
|     if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"): | ||||
|         if os.path.exists(settings.CERT_FILE) and os.path.exists(settings.KEY_FILE): | ||||
|             cert_file = settings.CERT_FILE | ||||
|             key_file = settings.KEY_FILE | ||||
|         else: | ||||
|             cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" | ||||
|             key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" | ||||
|     else: | ||||
|         cert_path = "/opt/tactical/certs" | ||||
|         cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" | ||||
|         key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" | ||||
|  | ||||
|     config = { | ||||
|         "tls": { | ||||
|             "cert_file": f"{cert_path}/fullchain.pem", | ||||
|             "key_file": f"{cert_path}/privkey.pem", | ||||
|             "cert_file": cert_file, | ||||
|             "key_file": key_file, | ||||
|         }, | ||||
|         "authorization": {"users": users}, | ||||
|         "max_payload": 2048576005, | ||||
| @@ -39,6 +118,7 @@ def reload_nats(): | ||||
|         json.dump(config, f) | ||||
|  | ||||
|     if not settings.DOCKER_BUILD: | ||||
|         time.sleep(0.5) | ||||
|         subprocess.run( | ||||
|             ["/usr/local/bin/nats-server", "-signal", "reload"], capture_output=True | ||||
|         ) | ||||
|   | ||||
| @@ -107,7 +107,7 @@ def check_agent_update_schedule_task(): | ||||
| def check_for_updates_task(pk, wait=False, auto_approve=False): | ||||
|  | ||||
|     if wait: | ||||
|         sleep(70) | ||||
|         sleep(120) | ||||
|  | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     ret = agent.salt_api_cmd( | ||||
|   | ||||
| @@ -175,7 +175,7 @@ class WinupdateTasks(TacticalTestCase): | ||||
|         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") | ||||
|     """ @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 | ||||
|  | ||||
| @@ -204,7 +204,7 @@ class WinupdateTasks(TacticalTestCase): | ||||
|  | ||||
|         check_agent_update_schedule_task() | ||||
|         agent_salt_cmd.assert_called_with(func="win_agent.install_updates") | ||||
|         self.assertEquals(agent_salt_cmd.call_count, 2) | ||||
|         self.assertEquals(agent_salt_cmd.call_count, 2) """ | ||||
|  | ||||
|     @patch("agents.models.Agent.salt_api_cmd") | ||||
|     def test_check_for_updates(self, salt_api_cmd): | ||||
|   | ||||
| @@ -1,12 +1,9 @@ | ||||
| from django.urls import path | ||||
| from . import views | ||||
| from apiv3 import views as v3_views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("<int:pk>/getwinupdates/", views.get_win_updates), | ||||
|     path("<int:pk>/runupdatescan/", views.run_update_scan), | ||||
|     path("editpolicy/", views.edit_policy), | ||||
|     path("winupdater/", views.win_updater), | ||||
|     path("results/", v3_views.WinUpdater.as_view()), | ||||
|     path("<int:pk>/installnow/", views.install_updates), | ||||
| ] | ||||
|   | ||||
| @@ -58,20 +58,3 @@ def edit_policy(request): | ||||
|     patch.action = request.data["policy"] | ||||
|     patch.save(update_fields=["action"]) | ||||
|     return Response("ok") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @authentication_classes((TokenAuthentication,)) | ||||
| @permission_classes((IsAuthenticated,)) | ||||
| def win_updater(request): | ||||
|     agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|     agent.delete_superseded_updates() | ||||
|     patches = ( | ||||
|         WinUpdate.objects.filter(agent=agent) | ||||
|         .exclude(installed=True) | ||||
|         .filter(action="approve") | ||||
|     ) | ||||
|     if patches: | ||||
|         return Response(ApprovedUpdateSerializer(patches, many=True).data) | ||||
|  | ||||
|     return Response("nopatches") | ||||
| @@ -27,15 +27,21 @@ jobs: | ||||
|           source env/bin/activate | ||||
|           cd /myagent/_work/1/s/api/tacticalrmm | ||||
|           pip install --no-cache-dir --upgrade pip | ||||
|           pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1 | ||||
|           pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1 | ||||
|           pip install --no-cache-dir -r requirements.txt -r requirements-test.txt | ||||
|         displayName: "Install Python Dependencies" | ||||
|  | ||||
|       - script: | | ||||
|           cd /myagent/_work/1/s/api | ||||
|           git config user.email "admin@example.com" | ||||
|           git config user.name "Bob" | ||||
|           git fetch | ||||
|           git checkout develop | ||||
|           git pull | ||||
|           source env/bin/activate | ||||
|           cd /myagent/_work/1/s/api/tacticalrmm | ||||
|           python manage.py test -v 2 | ||||
|           coverage run manage.py test -v 2 | ||||
|           coveralls | ||||
|         displayName: "Run django tests" | ||||
|  | ||||
|       - script: | | ||||
|   | ||||
							
								
								
									
										16
									
								
								backup.sh
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								backup.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="3" | ||||
| SCRIPT_VERSION="5" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| @@ -50,6 +50,11 @@ if [ -d /meshcentral/meshcentral-coredumps ]; then | ||||
|     rm -f /meshcentral/meshcentral-coredumps/* | ||||
| fi | ||||
|  | ||||
| printf >&2 "${GREEN}Running postgres vacuum${NC}\n" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage" | ||||
|  | ||||
| dt_now=$(date '+%Y_%m_%d__%H_%M_%S') | ||||
| tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX) | ||||
| sysd="/etc/systemd/system" | ||||
| @@ -72,18 +77,13 @@ mongodump --gzip --out=${tmp_dir}/meshcentral/mongo | ||||
| sudo tar -czvf ${tmp_dir}/salt/etc-salt.tar.gz -C /etc/salt . | ||||
| tar -czvf ${tmp_dir}/salt/srv-salt.tar.gz -C /srv/salt . | ||||
|  | ||||
| if [ -d "/certs" ]; then | ||||
|   sudo tar -czvf ${tmp_dir}/certs/certs.tar.gz -C /certs . | ||||
| else | ||||
|   sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . | ||||
| fi | ||||
|  | ||||
| sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . | ||||
|  | ||||
| sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx . | ||||
|  | ||||
| sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d . | ||||
|  | ||||
| sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${tmp_dir}/systemd/ | ||||
| sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/ | ||||
|  | ||||
| cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz | ||||
| cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/ | ||||
|   | ||||
| @@ -19,7 +19,7 @@ FROM nginx:stable-alpine | ||||
| ENV PUBLIC_DIR /usr/share/nginx/html | ||||
|  | ||||
| RUN apk add --no-cache bash | ||||
| SHELL ["/bin/bash", "-c"] | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR} | ||||
|  | ||||
|   | ||||
| @@ -6,9 +6,12 @@ ENV TACTICAL_DIR /opt/tactical | ||||
|  | ||||
| RUN apk add --no-cache bash | ||||
|  | ||||
| SHELL ["/bin/bash", "-c"] | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| RUN npm install meshcentral@0.6.62 | ||||
| COPY api/tacticalrmm/tacticalrmm/settings.py /tmp/settings.py | ||||
|  | ||||
| RUN grep -o 'MESH_VER.*' /tmp/settings.py | cut -d'"' -f 2 > /tmp/MESH_VER && \ | ||||
|     npm install meshcentral@$(cat /tmp/MESH_VER) | ||||
|  | ||||
| COPY docker/containers/tactical-meshcentral/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|   | ||||
| @@ -62,5 +62,11 @@ if [ ! -f "${TACTICAL_DIR}/tmp/mesh_token" ]; then | ||||
|     node node_modules/meshcentral --logintokenkey > ${TACTICAL_DIR}/tmp/mesh_token | ||||
| fi | ||||
|  | ||||
| # wait for nginx container | ||||
| until (echo > /dev/tcp/"${NGINX_HOST_IP}"/443) &> /dev/null; do | ||||
|   echo "waiting for nginx to start..." | ||||
|   sleep 5 | ||||
| done | ||||
|  | ||||
| # start mesh | ||||
| node node_modules/meshcentral | ||||
|   | ||||
| @@ -5,7 +5,7 @@ ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
|  | ||||
| RUN apk add --no-cache inotify-tools supervisor bash | ||||
|  | ||||
| SHELL ["/bin/bash", "-c"] | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| COPY docker/containers/tactical-nats/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|   | ||||
| @@ -11,6 +11,9 @@ done | ||||
| mkdir -p /var/log/supervisor | ||||
| mkdir -p /etc/supervisor/conf.d | ||||
|  | ||||
| # wait for config changes | ||||
|  | ||||
|  | ||||
| supervisor_config="$(cat << EOF | ||||
| [supervisord] | ||||
| nodaemon=true | ||||
| @@ -18,13 +21,13 @@ nodaemon=true | ||||
| files = /etc/supervisor/conf.d/*.conf | ||||
|  | ||||
| [program:nats-server] | ||||
| command=nats-server -DVV --config "${TACTICAL_DIR}/api/nats-rmm.conf" | ||||
| command=nats-server --config ${TACTICAL_DIR}/api/nats-rmm.conf | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|  | ||||
| [program:config-watcher] | ||||
| command="inotifywait -m -e close_write ${TACTICAL_DIR}/api/nats-rmm.conf"; | while read events; do "nats-server --signal reload"; done; | ||||
| command=/bin/bash -c "inotifywait -mq -e modify "${TACTICAL_DIR}/api/nats-rmm.conf" | while read event; do nats-server --signal reload; done;" | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|   | ||||
| @@ -4,7 +4,7 @@ ENV TACTICAL_DIR /opt/tactical | ||||
|  | ||||
| RUN apk add --no-cache openssl bash | ||||
|  | ||||
| SHELL ["/bin/bash", "-c"] | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/ | ||||
| RUN chmod +x /docker-entrypoint.d/entrypoint.sh | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| FROM ubuntu:20.04 | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
| ENV SALT_USER saltapi | ||||
|  | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \ | ||||
|     wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \ | ||||
|   | ||||
| @@ -4,10 +4,10 @@ set -e | ||||
|  | ||||
| : "${SALT_USER:='saltapi'}" | ||||
|  | ||||
| # wait for salt password to be generated by tactical-init | ||||
| until [ -f "${TACTICAL_DIR}/tmp/salt_pass" ]; do | ||||
|     echo "waiting for salt password to be generated..." | ||||
|     sleep 10 | ||||
| sleep 15 | ||||
| until [ -f "${TACTICAL_READY_FILE}" ]; do | ||||
|   echo "waiting for init container to finish install or update..." | ||||
|   sleep 10 | ||||
| done | ||||
|  | ||||
| SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass) | ||||
| @@ -15,7 +15,10 @@ SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass) | ||||
| echo "${SALT_USER}:${SALT_PASS}" | chpasswd | ||||
|  | ||||
| cherrypy_config="$(cat << EOF | ||||
| module_dirs: ['/opt/tactical/_modules'] | ||||
| file_roots: | ||||
|   base: | ||||
|     - /srv/salt | ||||
|     - ${TACTICAL_DIR} | ||||
| timeout: 20 | ||||
| gather_job_timeout: 25 | ||||
| max_event_size: 30485760 | ||||
| @@ -42,11 +45,15 @@ nodaemon=true | ||||
| files = /etc/supervisor/conf.d/*.conf | ||||
|  | ||||
| [program:salt-master] | ||||
| command=/bin/bash -c "salt-master -l debug" | ||||
| command=/bin/bash -c "salt-master -l info" | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|  | ||||
| [program:salt-api] | ||||
| command=/bin/bash -c "salt-api -l debug" | ||||
| command=/bin/bash -c "salt-api -l info" | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
| EOF | ||||
| )" | ||||
|   | ||||
| @@ -69,6 +69,9 @@ DEBUG = False | ||||
|  | ||||
| DOCKER_BUILD = True | ||||
|  | ||||
| CERT_FILE = '/opt/tactical/certs/fullchain.pem' | ||||
| KEY_FILE = '/opt/tactical/certs/privkey.pem' | ||||
|  | ||||
| SCRIPTS_DIR = '/opt/tactical/scripts' | ||||
|  | ||||
| ALLOWED_HOSTS = ['${API_HOST}'] | ||||
| @@ -177,4 +180,4 @@ fi | ||||
| if [ "$1" = 'tactical-celerywinupdate' ]; then | ||||
|   check_tactical_ready | ||||
|   celery -A tacticalrmm worker -Q wupdate | ||||
| fi | ||||
| fi | ||||
|   | ||||
| @@ -51,6 +51,7 @@ services: | ||||
|       POSTGRES_PASS: ${POSTGRES_PASS} | ||||
|       APP_HOST: ${APP_HOST} | ||||
|       API_HOST: ${API_HOST} | ||||
|       MESH_USER: ${MESH_USER} | ||||
|       MESH_HOST: ${MESH_HOST} | ||||
|       TRMM_USER: ${TRMM_USER} | ||||
|       TRMM_PASS: ${TRMM_PASS} | ||||
| @@ -100,8 +101,10 @@ services: | ||||
|       MONGODB_USER: ${MONGODB_USER} | ||||
|       MONGODB_PASSWORD: ${MONGODB_PASSWORD} | ||||
|     networks: | ||||
|       - proxy | ||||
|       - mesh-db | ||||
|       proxy: | ||||
|         aliases: | ||||
|           - ${MESH_HOST} | ||||
|       mesh-db: | ||||
|     volumes: | ||||
|       - tactical_data:/opt/tactical | ||||
|       - mesh_data:/home/node/app/meshcentral-data | ||||
|   | ||||
							
								
								
									
										85
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								install.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="23" | ||||
| SCRIPT_VERSION="27" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh' | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| @@ -205,12 +205,47 @@ sudo apt install -y mongodb-org | ||||
| sudo systemctl enable mongod | ||||
| sudo systemctl restart mongod | ||||
|  | ||||
|  | ||||
|  | ||||
| print_green 'Installing python, redis and git' | ||||
|  | ||||
| sudo apt update | ||||
| sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git | ||||
|  | ||||
| print_green 'Installing postgresql' | ||||
|  | ||||
| sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' | ||||
| wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - | ||||
| sudo apt update | ||||
| sudo apt install -y postgresql-13 | ||||
|  | ||||
| print_green 'Creating database for the rmm' | ||||
|  | ||||
| sudo -u postgres psql -c "CREATE DATABASE tacticalrmm" | ||||
| sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'" | ||||
| sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}" | ||||
|  | ||||
| sudo mkdir /rmm | ||||
| sudo chown ${USER}:${USER} /rmm | ||||
| sudo mkdir -p /var/log/celery | ||||
| sudo chown ${USER}:${USER} /var/log/celery | ||||
| git clone https://github.com/wh1te909/tacticalrmm.git /rmm/ | ||||
| cd /rmm | ||||
| git config user.email "admin@example.com" | ||||
| git config user.name "Bob" | ||||
| git checkout master | ||||
|  | ||||
| print_green 'Installing MeshCentral' | ||||
|  | ||||
| MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') | ||||
|  | ||||
| sudo mkdir -p /meshcentral/meshcentral-data | ||||
| sudo chown ${USER}:${USER} -R /meshcentral | ||||
| cd /meshcentral | ||||
| npm install meshcentral@0.6.84 | ||||
| npm install meshcentral@${MESH_VER} | ||||
| sudo chown ${USER}:${USER} -R /meshcentral | ||||
|  | ||||
| meshcfg="$(cat << EOF | ||||
| @@ -253,37 +288,6 @@ EOF | ||||
| )" | ||||
| echo "${meshcfg}" > /meshcentral/meshcentral-data/config.json | ||||
|  | ||||
| print_green 'Installing python, redis and git' | ||||
|  | ||||
| sudo apt update | ||||
| sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git | ||||
|  | ||||
| print_green 'Installing postgresql' | ||||
|  | ||||
| sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' | ||||
| wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - | ||||
| sudo apt update | ||||
| sudo apt install -y postgresql-13 | ||||
|  | ||||
| print_green 'Creating database for the rmm' | ||||
|  | ||||
| sudo -u postgres psql -c "CREATE DATABASE tacticalrmm" | ||||
| sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'" | ||||
| sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}" | ||||
|  | ||||
| sudo mkdir /rmm | ||||
| sudo chown ${USER}:${USER} /rmm | ||||
| sudo mkdir -p /var/log/celery | ||||
| sudo chown ${USER}:${USER} /var/log/celery | ||||
| git clone https://github.com/wh1te909/tacticalrmm.git /rmm/ | ||||
| cd /rmm | ||||
| git config user.email "admin@example.com" | ||||
| git config user.name "Bob" | ||||
| git checkout master | ||||
|  | ||||
| localvars="$(cat << EOF | ||||
| SECRET_KEY = "${DJANGO_SEKRET}" | ||||
|  | ||||
| @@ -348,7 +352,7 @@ python3 -m venv env | ||||
| source /rmm/api/env/bin/activate | ||||
| cd /rmm/api/tacticalrmm | ||||
| pip install --no-cache-dir --upgrade pip | ||||
| pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1 | ||||
| pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1 | ||||
| pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt | ||||
| python manage.py migrate | ||||
| python manage.py collectstatic --no-input | ||||
| @@ -504,14 +508,7 @@ nginxmesh="$(cat << EOF | ||||
| server { | ||||
|   listen 80; | ||||
|   server_name ${meshdomain}; | ||||
|   location / { | ||||
|      proxy_pass http://127.0.0.1:800; | ||||
|      proxy_http_version 1.1; | ||||
|      proxy_set_header X-Forwarded-Host \$host:\$server_port; | ||||
|      proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | ||||
|      proxy_set_header X-Forwarded-Proto \$scheme; | ||||
|   } | ||||
|  | ||||
|   return 301 https://\$server_name\$request_uri; | ||||
| } | ||||
|  | ||||
| server { | ||||
| @@ -530,6 +527,7 @@ server { | ||||
|         proxy_pass http://127.0.0.1:4430/; | ||||
|         proxy_http_version 1.1; | ||||
|  | ||||
|         proxy_set_header Host \$host; | ||||
|         proxy_set_header Upgrade \$http_upgrade; | ||||
|         proxy_set_header Connection "upgrade"; | ||||
|         proxy_set_header X-Forwarded-Host \$host:\$server_port; | ||||
| @@ -557,7 +555,6 @@ sleep 30 | ||||
|  | ||||
| saltvars="$(cat << EOF | ||||
| timeout: 20 | ||||
| worker_threads: 15 | ||||
| gather_job_timeout: 25 | ||||
| max_event_size: 30485760 | ||||
| external_auth: | ||||
| @@ -572,8 +569,6 @@ rest_cherrypy: | ||||
|   port: 8123 | ||||
|   disable_ssl: True | ||||
|   max_request_body_size: 30485760 | ||||
|   thread_pool: 300 | ||||
|   socket_queue_size: 100 | ||||
|  | ||||
| EOF | ||||
| )" | ||||
|   | ||||
							
								
								
									
										62
									
								
								restore.sh
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								restore.sh
									
									
									
									
									
								
							| @@ -7,7 +7,7 @@ pgpw="hunter2" | ||||
|  | ||||
| ##################################################### | ||||
|  | ||||
| SCRIPT_VERSION="6" | ||||
| SCRIPT_VERSION="10" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh' | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| @@ -93,13 +93,25 @@ sudo apt update | ||||
| sudo apt install -y curl wget | ||||
| sudo mkdir -p /usr/local/rmmgo | ||||
| go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX) | ||||
| wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P ${go_tmp} | ||||
| wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P ${go_tmp} | ||||
|  | ||||
| tar -xzf ${go_tmp}/go1.15.linux-amd64.tar.gz -C ${go_tmp} | ||||
| tar -xzf ${go_tmp}/go1.15.5.linux-amd64.tar.gz -C ${go_tmp} | ||||
|  | ||||
| sudo mv ${go_tmp}/go /usr/local/rmmgo/ | ||||
| rm -rf ${go_tmp} | ||||
|  | ||||
| print_green 'Downloading NATS' | ||||
|  | ||||
| nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) | ||||
| wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp} | ||||
|  | ||||
| tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp} | ||||
|  | ||||
| sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/ | ||||
| sudo chmod +x /usr/local/bin/nats-server | ||||
| sudo chown ${USER}:${USER} /usr/local/bin/nats-server | ||||
| rm -rf ${nats_tmp} | ||||
|  | ||||
| print_green 'Installing NodeJS' | ||||
|  | ||||
| curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - | ||||
| @@ -133,15 +145,13 @@ print_green 'Restoring certbot' | ||||
| sudo apt install -y software-properties-common | ||||
| sudo apt install -y certbot openssl | ||||
|  | ||||
| if [ -f "${tmp_dir}/certs/certs.tar.gz" ]; then | ||||
|   sudo mkdir /certs | ||||
|   sudo tar -xzf $tmp_dir/certs/certs.tar.gz -C /certs | ||||
| else | ||||
|   sudo rm -rf /etc/letsencrypt | ||||
|   sudo mkdir /etc/letsencrypt | ||||
|   sudo tar -xzf $tmp_dir/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt | ||||
| fi | ||||
| print_green 'Restoring certs' | ||||
|  | ||||
| sudo rm -rf /etc/letsencrypt | ||||
| sudo mkdir /etc/letsencrypt | ||||
| sudo tar -xzf $tmp_dir/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt | ||||
| sudo chown ${USER}:${USER} -R /etc/letsencrypt | ||||
| sudo chmod 775 -R /etc/letsencrypt | ||||
|  | ||||
| print_green 'Restoring celery configs' | ||||
|  | ||||
| @@ -169,7 +179,7 @@ print_green 'Installing postgresql' | ||||
| sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' | ||||
| wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - | ||||
| sudo apt update | ||||
| sudo apt install -y postgresql-12 | ||||
| sudo apt install -y postgresql-13 | ||||
| sleep 2 | ||||
|  | ||||
| print_green 'Restoring the database' | ||||
| @@ -197,15 +207,6 @@ sudo systemctl restart mongod | ||||
| sleep 5 | ||||
| mongorestore --gzip $tmp_dir/meshcentral/mongo | ||||
|  | ||||
| print_green 'Restoring MeshCentral' | ||||
|  | ||||
| sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C / | ||||
| sudo chown ${USER}:${USER} -R /meshcentral | ||||
| cd /meshcentral | ||||
| npm install meshcentral@0.6.62 | ||||
|  | ||||
|  | ||||
| print_green 'Restoring the backend' | ||||
|  | ||||
| sudo mkdir /rmm | ||||
| sudo chown ${USER}:${USER} /rmm | ||||
| @@ -213,8 +214,21 @@ sudo mkdir -p /var/log/celery | ||||
| sudo chown ${USER}:${USER} /var/log/celery | ||||
| git clone https://github.com/wh1te909/tacticalrmm.git /rmm/ | ||||
| cd /rmm | ||||
| git config user.email "admin@example.com" | ||||
| git config user.name "Bob" | ||||
| git checkout master | ||||
|  | ||||
| print_green 'Restoring MeshCentral' | ||||
|  | ||||
| MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') | ||||
| sudo tar -xzf $tmp_dir/meshcentral/mesh.tar.gz -C / | ||||
| sudo chown ${USER}:${USER} -R /meshcentral | ||||
| cd /meshcentral | ||||
| npm install meshcentral@${MESH_VER} | ||||
|  | ||||
|  | ||||
| print_green 'Restoring the backend' | ||||
|  | ||||
| cp $tmp_dir/rmm/local_settings.py /rmm/api/tacticalrmm/tacticalrmm/ | ||||
| cp $tmp_dir/rmm/env /rmm/web/.env | ||||
| cp $tmp_dir/rmm/app.ini /rmm/api/tacticalrmm/ | ||||
| @@ -232,11 +246,15 @@ python3 -m venv env | ||||
| source /rmm/api/env/bin/activate | ||||
| cd /rmm/api/tacticalrmm | ||||
| pip install --no-cache-dir --upgrade pip | ||||
| pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1 | ||||
| pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1 | ||||
| pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt | ||||
| python manage.py collectstatic --no-input | ||||
| python manage.py reload_nats | ||||
| deactivate | ||||
|  | ||||
| sudo systemctl enable nats.service | ||||
| sudo systemctl start nats.service | ||||
|  | ||||
|  | ||||
| print_green 'Installing Salt Master' | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="98" | ||||
| SCRIPT_VERSION="100" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh' | ||||
| LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py' | ||||
| YELLOW='\033[1;33m' | ||||
| @@ -177,6 +177,11 @@ sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/ | ||||
| sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo | ||||
| sudo chmod +x /usr/local/bin/goversioninfo | ||||
|  | ||||
| printf >&2 "${GREEN}Running postgres vacuum${NC}\n" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction" | ||||
| sudo -u postgres psql -d tacticalrmm -c "vacuum full agents_agentoutage" | ||||
|  | ||||
| if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then | ||||
|   rm -rf /rmm/api/env | ||||
|   cd /rmm/api | ||||
| @@ -184,7 +189,7 @@ if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then | ||||
|   source /rmm/api/env/bin/activate | ||||
|   cd /rmm/api/tacticalrmm | ||||
|   pip install --no-cache-dir --upgrade pip | ||||
|   pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1 | ||||
|   pip install --no-cache-dir setuptools==50.3.2 wheel==0.36.1 | ||||
|   pip install --no-cache-dir -r requirements.txt | ||||
| else | ||||
|   source /rmm/api/env/bin/activate | ||||
|   | ||||
							
								
								
									
										856
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										856
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -13,10 +13,10 @@ | ||||
|     "axios": "^0.21.0", | ||||
|     "dotenv": "^8.2.0", | ||||
|     "qrcode.vue": "^1.7.0", | ||||
|     "quasar": "^1.14.5" | ||||
|     "quasar": "^1.14.6" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@quasar/app": "^2.1.8", | ||||
|     "@quasar/app": "^2.1.9", | ||||
|     "@quasar/cli": "^1.1.2", | ||||
|     "@quasar/quasar-app-extension-testing": "^1.0.0", | ||||
|     "@quasar/quasar-app-extension-testing-unit-jest": "^1.0.1", | ||||
|   | ||||
| @@ -46,6 +46,13 @@ | ||||
|           </q-icon> | ||||
|         </q-th> | ||||
|       </template> | ||||
|       <template v-slot:header-cell-pendingactions="props"> | ||||
|         <q-th auto-width :props="props"> | ||||
|           <q-icon name="far fa-clock" size="1.5em"> | ||||
|             <q-tooltip>Pending Actions</q-tooltip> | ||||
|           </q-icon> | ||||
|         </q-th> | ||||
|       </template> | ||||
|       <!-- | ||||
|       <template v-slot:header-cell-antivirus="props"> | ||||
|         <q-th auto-width :props="props"> | ||||
| @@ -101,14 +108,6 @@ | ||||
|                 <q-item-section>Take Control</q-item-section> | ||||
|               </q-item> | ||||
|  | ||||
|               <!-- web rdp --> | ||||
|               <q-item clickable v-ripple v-close-popup @click.stop.prevent="webRDP(props.row.id)"> | ||||
|                 <q-item-section side> | ||||
|                   <q-icon size="xs" name="screen_share" /> | ||||
|                 </q-item-section> | ||||
|                 <q-item-section>Remote Desktop</q-item-section> | ||||
|               </q-item> | ||||
|  | ||||
|               <q-item clickable v-ripple v-close-popup @click="showSendCommand = true"> | ||||
|                 <q-item-section side> | ||||
|                   <q-icon size="xs" name="fas fa-terminal" /> | ||||
| @@ -274,6 +273,18 @@ | ||||
|               <q-tooltip>Patches Pending</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
|           <q-td :props="props" key="pendingactions"> | ||||
|             <q-icon | ||||
|               v-if="props.row.pending_actions !== 0" | ||||
|               @click="showPendingActionsModal(props.row.id)" | ||||
|               name="far fa-clock" | ||||
|               size="1.4em" | ||||
|               color="warning" | ||||
|               class="cursor-pointer" | ||||
|             > | ||||
|               <q-tooltip>Pending Action Count: {{ props.row.pending_actions }}</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
|           <q-td key="agentstatus"> | ||||
|             <q-icon v-if="props.row.status === 'overdue'" name="fas fa-signal" size="1.2em" color="negative"> | ||||
|               <q-tooltip>Agent overdue</q-tooltip> | ||||
| @@ -305,12 +316,12 @@ | ||||
|     </q-dialog> | ||||
|     <!-- reboot later modal --> | ||||
|     <q-dialog v-model="showRebootLaterModal"> | ||||
|       <RebootLater @close="showRebootLaterModal = false" /> | ||||
|       <RebootLater @close="showRebootLaterModal = false" @edited="agentEdited" /> | ||||
|     </q-dialog> | ||||
|     <!-- pending actions modal --> | ||||
|     <div class="q-pa-md q-gutter-sm"> | ||||
|       <q-dialog v-model="showPendingActions" @hide="closePendingActionsModal"> | ||||
|         <PendingActions :agentpk="pendingActionAgentPk" @close="closePendingActionsModal" /> | ||||
|         <PendingActions :agentpk="pendingActionAgentPk" @close="closePendingActionsModal" @edited="agentEdited" /> | ||||
|       </q-dialog> | ||||
|     </div> | ||||
|     <!-- add policy modal --> | ||||
| @@ -384,6 +395,7 @@ export default { | ||||
|       let availability = null; | ||||
|       let checks = false; | ||||
|       let patches = false; | ||||
|       let actions = false; | ||||
|       let reboot = false; | ||||
|       let search = ""; | ||||
|  | ||||
| @@ -394,6 +406,7 @@ export default { | ||||
|           advancedFilter = true; | ||||
|           let filter = param.split(":")[1]; | ||||
|           if (filter === "patchespending") patches = true; | ||||
|           if (filter === "actionspending") actions = true; | ||||
|           else if (filter === "checksfailing") checks = true; | ||||
|           else if (filter === "rebootneeded") reboot = true; | ||||
|           else if (filter === "online" || filter === "offline" || filter === "expired") availability = filter; | ||||
| @@ -406,6 +419,7 @@ export default { | ||||
|         if (advancedFilter) { | ||||
|           if (checks && !row.checks.has_failing_checks) return false; | ||||
|           if (patches && !row.patches_pending) return false; | ||||
|           if (actions && row.pending_actions > 0) return false; | ||||
|           if (reboot && !row.needs_reboot) return false; | ||||
|           if (availability === "online" && row.status !== "online") return false; | ||||
|           else if (availability === "offline" && row.status !== "overdue") return false; | ||||
| @@ -548,13 +562,17 @@ export default { | ||||
|           persistent: true, | ||||
|         }) | ||||
|         .onOk(() => { | ||||
|           const data = { pk: pk, action: "rebootnow" }; | ||||
|           axios.post("/agents/poweraction/", data).then(r => { | ||||
|             this.$q.dialog({ | ||||
|               title: `Restarting ${hostname}`, | ||||
|               message: `${hostname} will now be restarted`, | ||||
|           this.$q.loading.show(); | ||||
|           this.$axios | ||||
|             .post("/agents/reboot/", { pk: pk }) | ||||
|             .then(r => { | ||||
|               this.$q.loading.hide(); | ||||
|               this.notifySuccess(`${hostname} will now be restarted`); | ||||
|             }) | ||||
|             .catch(e => { | ||||
|               this.$q.loading.hide(); | ||||
|               this.notifyError(e.response.data); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|     }, | ||||
|     agentRowSelected(pk) { | ||||
| @@ -598,19 +616,6 @@ export default { | ||||
|       this.policyAddPk = pk; | ||||
|       this.showPolicyAddModal = true; | ||||
|     }, | ||||
|     webRDP(pk) { | ||||
|       this.$q.loading.show(); | ||||
|       this.$axios | ||||
|         .get(`/agents/${pk}/meshcentral/`) | ||||
|         .then(r => { | ||||
|           this.$q.loading.hide(); | ||||
|           openURL(r.data.webrdp); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifyError(e.response.data); | ||||
|         }); | ||||
|     }, | ||||
|     toggleMaintenance(agent) { | ||||
|       let data = { | ||||
|         id: agent.id, | ||||
|   | ||||
| @@ -119,6 +119,7 @@ | ||||
|     <q-separator /> | ||||
|     <q-card-section> | ||||
|       <q-table | ||||
|         @request="onRequest" | ||||
|         dense | ||||
|         :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }" | ||||
|         class="audit-mgr-tbl-sticky" | ||||
| @@ -131,6 +132,7 @@ | ||||
|         :rows-per-page-options="[25, 50, 100, 500, 1000]" | ||||
|         :no-data-label="noDataText" | ||||
|         @row-click="showDetails" | ||||
|         virtual-scroll | ||||
|       > | ||||
|         <template v-slot:top-right> | ||||
|           <q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportLog" /> | ||||
| @@ -263,8 +265,10 @@ export default { | ||||
|       ], | ||||
|       pagination: { | ||||
|         rowsPerPage: 25, | ||||
|         rowsNumber: null, | ||||
|         sortBy: "entry_time", | ||||
|         descending: true, | ||||
|         page: 1, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| @@ -355,10 +359,23 @@ export default { | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|     onRequest(props) { | ||||
|       // needed to update external pagination object | ||||
|       const { page, rowsPerPage, sortBy, descending } = props.pagination; | ||||
|  | ||||
|       this.pagination.page = page; | ||||
|       this.pagination.rowsPerPage = rowsPerPage; | ||||
|       this.pagination.sortBy = sortBy; | ||||
|       this.pagination.descending = descending; | ||||
|  | ||||
|       this.search(); | ||||
|     }, | ||||
|     search() { | ||||
|       this.$q.loading.show(); | ||||
|       this.searched = true; | ||||
|       let data = {}; | ||||
|       let data = { | ||||
|         pagination: this.pagination, | ||||
|       }; | ||||
|  | ||||
|       if (!!this.agentFilter && this.agentFilter.length > 0) data["agentFilter"] = this.agentFilter; | ||||
|       else if (!!this.clientFilter && this.clientFilter.length > 0) data["clientFilter"] = this.clientFilter; | ||||
| @@ -371,7 +388,8 @@ export default { | ||||
|         .patch("/logs/auditlogs/", data) | ||||
|         .then(r => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.auditLogs = Object.freeze(r.data); | ||||
|           this.auditLogs = Object.freeze(r.data.audit_logs); | ||||
|           this.pagination.rowsNumber = r.data.total; | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.$q.loading.hide(); | ||||
|   | ||||
| @@ -129,7 +129,7 @@ | ||||
|                   dense | ||||
|                   @input="checkAlert(props.row.id, 'Text', props.row.text_alert, props.row.managed_by_policy)" | ||||
|                   v-model="props.row.text_alert" | ||||
|                   :disabled="props.row.managed_by_policy" | ||||
|                   :disable="props.row.managed_by_policy" | ||||
|                 /> | ||||
|               </q-td> | ||||
|               <!-- email alert --> | ||||
| @@ -138,7 +138,7 @@ | ||||
|                   dense | ||||
|                   @input="checkAlert(props.row.id, 'Email', props.row.email_alert, props.row.managed_by_policy)" | ||||
|                   v-model="props.row.email_alert" | ||||
|                   :disabled="props.row.managed_by_policy" | ||||
|                   :disable="props.row.managed_by_policy" | ||||
|                 /> | ||||
|               </q-td> | ||||
|               <!-- policy check icon --> | ||||
|   | ||||
| @@ -130,6 +130,10 @@ | ||||
|               <q-item clickable v-close-popup @click="showBulkActionModal('scan')"> | ||||
|                 <q-item-section>Bulk Patch Management</q-item-section> | ||||
|               </q-item> | ||||
|               <!-- server maintenance --> | ||||
|               <q-item clickable v-close-popup @click="showServerMaintenance = true"> | ||||
|                 <q-item-section>Server Maintenance</q-item-section> | ||||
|               </q-item> | ||||
|             </q-list> | ||||
|           </q-menu> | ||||
|         </q-btn> | ||||
| @@ -174,7 +178,7 @@ | ||||
|       <!-- Update Agents Modal --> | ||||
|       <div class="q-pa-md q-gutter-sm"> | ||||
|         <q-dialog v-model="showUpdateAgentsModal" maximized transition-show="slide-up" transition-hide="slide-down"> | ||||
|           <UpdateAgents @close="showUpdateAgentsModal = false" /> | ||||
|           <UpdateAgents @close="showUpdateAgentsModal = false" @edited="edited" /> | ||||
|         </q-dialog> | ||||
|       </div> | ||||
|       <!-- Script Manager --> | ||||
| @@ -196,16 +200,18 @@ | ||||
|       <q-dialog v-model="showUploadMesh"> | ||||
|         <UploadMesh @close="showUploadMesh = false" /> | ||||
|       </q-dialog> | ||||
|  | ||||
|       <!-- Bulk action modal --> | ||||
|       <q-dialog v-model="showBulkAction" @hide="closeBulkActionModal" position="top"> | ||||
|         <BulkAction :mode="bulkMode" @close="closeBulkActionModal" /> | ||||
|       </q-dialog> | ||||
|  | ||||
|       <!-- Agent Deployment --> | ||||
|       <q-dialog v-model="showDeployment"> | ||||
|         <Deployment @close="showDeployment = false" /> | ||||
|       </q-dialog> | ||||
|       <!-- Server Maintenance --> | ||||
|       <q-dialog v-model="showServerMaintenance"> | ||||
|         <ServerMaintenance @close="showMaintenance = false" /> | ||||
|       </q-dialog> | ||||
|     </q-bar> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -225,6 +231,7 @@ import UploadMesh from "@/components/modals/core/UploadMesh"; | ||||
| import AuditManager from "@/components/AuditManager"; | ||||
| import BulkAction from "@/components/modals/agents/BulkAction"; | ||||
| import Deployment from "@/components/Deployment"; | ||||
| import ServerMaintenance from "@/components/modals/core/ServerMaintenance"; | ||||
|  | ||||
| export default { | ||||
|   name: "FileBar", | ||||
| @@ -243,10 +250,12 @@ export default { | ||||
|     AuditManager, | ||||
|     BulkAction, | ||||
|     Deployment, | ||||
|     ServerMaintenance, | ||||
|   }, | ||||
|   props: ["clients"], | ||||
|   data() { | ||||
|     return { | ||||
|       showServerMaintenance: false, | ||||
|       showClientFormModal: false, | ||||
|       showSiteFormModal: false, | ||||
|       clientOp: null, | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|       hide-bottom | ||||
|     > | ||||
|       <template v-slot:top> | ||||
|         <q-btn dense flat push @click="refreshServices" icon="refresh" /> | ||||
|         <q-btn dense flat push @click="getServices" icon="refresh" /> | ||||
|         <q-space /> | ||||
|         <q-input v-model="filter" outlined label="Search" dense clearable> | ||||
|           <template v-slot:prepend> | ||||
| @@ -242,7 +242,7 @@ export default { | ||||
|         .then(r => { | ||||
|           this.serviceDetailVisible = false; | ||||
|           this.serviceDetailsModal = false; | ||||
|           this.refreshServices(); | ||||
|           this.getServices(); | ||||
|           this.notifySuccess(`Service ${name} was edited!`); | ||||
|         }) | ||||
|         .catch(e => { | ||||
| @@ -303,7 +303,7 @@ export default { | ||||
|       this.$axios | ||||
|         .post("/services/serviceaction/", data) | ||||
|         .then(r => { | ||||
|           this.refreshServices(); | ||||
|           this.getServices(); | ||||
|           this.serviceDetailsModal = false; | ||||
|           this.notifySuccess(`Service ${fullname} was ${status}!`); | ||||
|         }) | ||||
| @@ -313,19 +313,9 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     getServices() { | ||||
|       this.$q.loading.show({ message: "Loading services..." }); | ||||
|       this.$axios | ||||
|         .get(`/services/${this.pk}/services/`) | ||||
|         .then(r => { | ||||
|           this.servicesData = [r.data][0].services; | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.notifyError(e.response.data); | ||||
|         }); | ||||
|     }, | ||||
|     refreshServices() { | ||||
|       this.$q.loading.show({ message: "Reloading services..." }); | ||||
|       this.$axios | ||||
|         .get(`/services/${this.pk}/refreshedservices/`) | ||||
|         .then(r => { | ||||
|           this.servicesData = [r.data][0].services; | ||||
|           this.$q.loading.hide(); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <div v-if="!this.selectedAgentPk">No agent selected</div> | ||||
|   <div v-else-if="!Array.isArray(software) || !software.length">No software</div> | ||||
|   <div v-else> | ||||
|     <div class="row q-pt-xs items-start"> | ||||
|       <q-btn | ||||
| @@ -31,7 +30,7 @@ | ||||
|       :pagination.sync="pagination" | ||||
|       binary-state-sort | ||||
|       hide-bottom | ||||
|       row-key="name" | ||||
|       row-key="id" | ||||
|       :loading="loading" | ||||
|     > | ||||
|       <template v-slot:loading> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| <template> | ||||
|   <div v-if="Object.keys(summary).length === 0">No agent selected</div> | ||||
|   <div v-else> | ||||
|     <q-btn class="q-mr-sm" dense flat push icon="refresh" @click="refreshSummary" /> | ||||
|     <span> | ||||
|       <b>{{ summary.hostname }}</b> | ||||
|       <span v-if="summary.maintenance_mode"> • <q-badge color="warning"> Maintenance Mode </q-badge> </span> | ||||
| @@ -87,8 +88,12 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from "vuex"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
|  | ||||
| export default { | ||||
|   name: "SummaryTab", | ||||
|   mixins: [mixins], | ||||
|   data() { | ||||
|     return {}; | ||||
|   }, | ||||
| @@ -96,8 +101,22 @@ export default { | ||||
|     awaitingSync(total, passing, failing) { | ||||
|       return total !== 0 && passing === 0 && failing === 0 ? true : false; | ||||
|     }, | ||||
|     refreshSummary() { | ||||
|       this.$q.loading.show(); | ||||
|       this.$axios | ||||
|         .get(`/agents/${this.selectedAgentPk}/wmi/`) | ||||
|         .then(r => { | ||||
|           this.$store.dispatch("loadSummary", this.selectedAgentPk); | ||||
|           this.$q.loading.hide(); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifyError(e.response.data); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters(["selectedAgentPk"]), | ||||
|     summary() { | ||||
|       return this.$store.state.agentSummary; | ||||
|     }, | ||||
|   | ||||
| @@ -26,23 +26,25 @@ | ||||
|       > | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>--log DEBUG</code> | ||||
|             <code>-log debug</code> | ||||
|           </q-badge> | ||||
|           <span>To enable verbose output during the install</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>--local-salt "C:\\<some folder or path>\\salt-minion-setup.exe"</code> | ||||
|             <code>-silent</code> | ||||
|           </q-badge> | ||||
|           <span> | ||||
|             To skip downloading the salt-minion during the install. Download it | ||||
|             <a v-if="info.arch === '64'" :href="info.data.salt64">here</a> | ||||
|             <a v-else :href="info.data.salt32">here</a> | ||||
|           </span> | ||||
|           <span>Do not popup any message boxes during install</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>--local-mesh "C:\\<some folder or path>\\meshagent.exe"</code> | ||||
|             <code>-nosalt</code> | ||||
|           </q-badge> | ||||
|           <span> Do not install salt during agent install. </span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>-local-mesh "C:\\<some folder or path>\\meshagent.exe"</code> | ||||
|           </q-badge> | ||||
|           <span> | ||||
|             To skip downloading the Mesh Agent during the install. Download it | ||||
| @@ -53,16 +55,10 @@ | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>--cert "C:\\<some folder or path>\\ca.pem"</code> | ||||
|             <code>-cert "C:\\<some folder or path>\\ca.pem"</code> | ||||
|           </q-badge> | ||||
|           <span> To use a domain CA </span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>--timeout NUMBER_IN_SECONDS</code> | ||||
|           </q-badge> | ||||
|           <span> To increase the default timeout of 900 seconds for the installer. Use on slow computers.</span> | ||||
|         </div> | ||||
|       </q-expansion-item> | ||||
|       <br /> | ||||
|       <p class="text-italic">Note: the auth token above will be valid for {{ info.expires }} hours.</p> | ||||
|   | ||||
| @@ -27,10 +27,13 @@ | ||||
|         <p>Fix issues with the Tactical Checkrunner windows service which handles running all checks.</p> | ||||
|       </q-card-section> | ||||
|       <q-card-section v-show="mode === 'salt'"> | ||||
|         <p>Fix issues with the salt-minion which handles windows updates, chocolatey and scheduled tasks.</p> | ||||
|         <p>Fix issues with the salt-minion which handles windows updates and chocolatey.</p> | ||||
|       </q-card-section> | ||||
|       <q-card-section v-show="mode === 'rpc'"> | ||||
|         <p>Fix issues with the Tactical RPC service which handles most of the agent's realtime functions.</p> | ||||
|         <p> | ||||
|           Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled | ||||
|           tasks. | ||||
|         </p> | ||||
|       </q-card-section> | ||||
|       <q-card-section v-show="mode === 'command'"> | ||||
|         <p>Run a shell command on the agent.</p> | ||||
|   | ||||
| @@ -74,7 +74,7 @@ | ||||
|                   <q-select outlined dense options-dense v-model="timezone" :options="allTimezones" class="col-8" /> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-10">Check interval:</div> | ||||
|                   <div class="col-10">Run checks every:</div> | ||||
|                   <q-input | ||||
|                     dense | ||||
|                     type="number" | ||||
| @@ -90,7 +90,7 @@ | ||||
|                   /> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-10">Send an overdue alert if the server has not reported in after:</div> | ||||
|                   <div class="col-10">Send an overdue alert if the agent has not reported in after:</div> | ||||
|                   <q-input | ||||
|                     dense | ||||
|                     type="number" | ||||
|   | ||||
| @@ -87,6 +87,9 @@ export default { | ||||
|   name: "InstallAgent", | ||||
|   mixins: [mixins], | ||||
|   components: { AgentDownload }, | ||||
|   props: { | ||||
|     sitepk: Number, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       client_options: [], | ||||
| @@ -110,8 +113,19 @@ export default { | ||||
|         .get("/clients/clients/") | ||||
|         .then(r => { | ||||
|           this.client_options = this.formatClientOptions(r.data); | ||||
|           this.client = this.client_options[0]; | ||||
|           this.site = this.sites[0]; | ||||
|           if (this.sitepk !== undefined && this.sitepk !== null) { | ||||
|             this.client_options.forEach(client => { | ||||
|               let site = client.sites.find(site => site.id === this.sitepk); | ||||
|  | ||||
|               if (site !== undefined) { | ||||
|                 this.client = client; | ||||
|                 this.site = { value: site.id, label: site.name }; | ||||
|               } | ||||
|             }); | ||||
|           } else { | ||||
|             this.client = this.client_options[0]; | ||||
|             this.site = this.sites[0]; | ||||
|           } | ||||
|           this.$q.loading.hide(); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|   | ||||
| @@ -93,7 +93,7 @@ | ||||
|       <div class="col-3">Day of month to run:</div> | ||||
|       <div class="col-4"></div> | ||||
|       <q-select | ||||
|         :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|         :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|         dense | ||||
|         class="col-5" | ||||
|         outlined | ||||
| @@ -107,7 +107,7 @@ | ||||
|       <div class="col-3">Scheduled Time:</div> | ||||
|       <div class="col-4"></div> | ||||
|       <q-select | ||||
|         :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|         :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|         dense | ||||
|         class="col-5" | ||||
|         outlined | ||||
| @@ -122,43 +122,43 @@ | ||||
|     > | ||||
|       <div class="q-gutter-sm"> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="1" | ||||
|           label="Monday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="2" | ||||
|           label="Tuesday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="3" | ||||
|           label="Wednesday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="4" | ||||
|           label="Thursday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="5" | ||||
|           label="Friday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="6" | ||||
|           label="Saturday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           :disable="winupdatepolicy.run_time_frequency === 'inherit'" | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="0" | ||||
|           label="Sunday" | ||||
| @@ -186,16 +186,13 @@ | ||||
|     <hr /> | ||||
|     <q-card-section class="row" v-if="!policy"> | ||||
|       <div class="col-5"> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.reprocess_failed_inherit" | ||||
|           label="Inherit failed patch settings" | ||||
|         /> | ||||
|         <q-checkbox v-model="winupdatepolicy.reprocess_failed_inherit" label="Inherit failed patch settings" /> | ||||
|       </div> | ||||
|     </q-card-section> | ||||
|     <q-card-section class="row"> | ||||
|       <div class="col-5"> | ||||
|         <q-checkbox | ||||
|           :disabled="winupdatepolicy.reprocess_failed_inherit" | ||||
|           :disable="winupdatepolicy.reprocess_failed_inherit" | ||||
|           v-model="winupdatepolicy.reprocess_failed" | ||||
|           label="Reprocess failed patches" | ||||
|         /> | ||||
| @@ -205,17 +202,17 @@ | ||||
|         <q-input | ||||
|           dense | ||||
|           v-model.number="winupdatepolicy.reprocess_failed_times" | ||||
|           :disabled="winupdatepolicy.reprocess_failed_inherit" | ||||
|           :disable="winupdatepolicy.reprocess_failed_inherit" | ||||
|           type="number" | ||||
|           filled | ||||
|           label="Times" | ||||
|           :rules="[ val => val > 0 || 'Must be greater than 0']" | ||||
|           :rules="[val => val > 0 || 'Must be greater than 0']" | ||||
|         /> | ||||
|       </div> | ||||
|       <div class="col-3"></div> | ||||
|       <q-checkbox | ||||
|         v-model="winupdatepolicy.email_if_fail" | ||||
|         :disabled="winupdatepolicy.reprocess_failed_inherit" | ||||
|         :disable="winupdatepolicy.reprocess_failed_inherit" | ||||
|         label="Send an email when patch installation fails" | ||||
|       /> | ||||
|     </q-card-section> | ||||
|   | ||||
| @@ -33,7 +33,6 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
| import { mapGetters } from "vuex"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import { date } from "quasar"; | ||||
| @@ -50,11 +49,12 @@ export default { | ||||
|     scheduleReboot() { | ||||
|       this.$q.loading.show({ message: "Contacting agent..." }); | ||||
|       const data = { pk: this.selectedAgentPk, datetime: this.datetime }; | ||||
|       axios | ||||
|         .post("/agents/rebootlater/", data) | ||||
|       this.$axios | ||||
|         .patch("/agents/reboot/", data) | ||||
|         .then(r => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.$emit("close"); | ||||
|           this.$emit("edited"); | ||||
|           this.confirmReboot(r.data); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|   | ||||
| @@ -77,6 +77,7 @@ export default { | ||||
|         .post("/agents/updateagents/", data) | ||||
|         .then(r => { | ||||
|           this.$emit("close"); | ||||
|           this.$emit("edited"); | ||||
|           this.notifySuccess("Agents will now be updated"); | ||||
|         }) | ||||
|         .catch(() => this.notifyError("Something went wrong")); | ||||
|   | ||||
							
								
								
									
										100
									
								
								web/src/components/modals/core/ServerMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								web/src/components/modals/core/ServerMaintenance.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| <template> | ||||
|   <q-card style="min-width: 400px"> | ||||
|     <q-card-section class="row"> | ||||
|       <q-card-actions align="left"> | ||||
|         <div class="text-h6">Server Maintenance</div> | ||||
|       </q-card-actions> | ||||
|       <q-space /> | ||||
|       <q-card-actions align="right"> | ||||
|         <q-btn v-close-popup flat round dense icon="close" /> | ||||
|       </q-card-actions> | ||||
|     </q-card-section> | ||||
|     <q-card-section> | ||||
|       <q-form @submit.prevent="submit"> | ||||
|         <q-card-section> | ||||
|           <q-select | ||||
|             :rules="[val => !!val || '*Required']" | ||||
|             outlined | ||||
|             options-dense | ||||
|             label="Actions" | ||||
|             v-model="action" | ||||
|             :options="actions" | ||||
|             emit-value | ||||
|             map-options | ||||
|             @input="clear" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section v-if="action === 'prune_db'"> | ||||
|           <q-checkbox v-model="prune_tables" val="agent_outages" label="Agent outage"> | ||||
|             <q-tooltip>Removes resolved agent outage records</q-tooltip> | ||||
|           </q-checkbox> | ||||
|           <q-checkbox v-model="prune_tables" val="audit_logs" label="Audit Log"> | ||||
|             <q-tooltip>Removes agent check results</q-tooltip> | ||||
|           </q-checkbox> | ||||
|           <q-checkbox v-model="prune_tables" val="pending_actions" label="Pending Actions"> | ||||
|             <q-tooltip>Removes completed pending actions</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-actions align="left"> | ||||
|           <q-btn label="Submit" color="primary" type="submit" class="full-width" /> | ||||
|         </q-card-actions> | ||||
|       </q-form> | ||||
|     </q-card-section> | ||||
|   </q-card> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
|  | ||||
| export default { | ||||
|   name: "ServerMaintenance", | ||||
|   mixins: [mixins], | ||||
|   data() { | ||||
|     return { | ||||
|       action: null, | ||||
|       prune_tables: [], | ||||
|       actions: [ | ||||
|         { | ||||
|           label: "Reload Nats Configuration", | ||||
|           value: "reload_nats", | ||||
|         }, | ||||
|         { | ||||
|           label: "Remove Orphaned Tasks", | ||||
|           value: "rm_orphaned_tasks", | ||||
|         }, | ||||
|         { | ||||
|           label: "Prune DB Tables", | ||||
|           value: "prune_db", | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     clear() { | ||||
|       this.prune_tables = []; | ||||
|     }, | ||||
|     submit() { | ||||
|       this.$q.loading.show(); | ||||
|  | ||||
|       let data = { | ||||
|         action: this.action, | ||||
|         prune_tables: this.prune_tables, | ||||
|       }; | ||||
|  | ||||
|       this.$axios | ||||
|         .post("core/servermaintenance/", data) | ||||
|         .then(r => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifySuccess(r.data); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifyError(e.data); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -11,7 +11,7 @@ | ||||
|         <div class="col"> | ||||
|           <q-btn | ||||
|             label="Cancel Action" | ||||
|             :disable="selectedRow === null || selectedStatus === 'completed' || actionType === 'taskaction'" | ||||
|             :disable="selectedRow === null || selectedStatus === 'completed' || actionType !== 'schedreboot'" | ||||
|             color="red" | ||||
|             icon="cancel" | ||||
|             dense | ||||
| @@ -98,26 +98,26 @@ export default { | ||||
|       hostname: "", | ||||
|       pagination: { | ||||
|         rowsPerPage: 0, | ||||
|         sortBy: "due", | ||||
|         descending: true, | ||||
|         sortBy: "status", | ||||
|         descending: false, | ||||
|       }, | ||||
|       all_columns: [ | ||||
|         { name: "id", field: "id" }, | ||||
|         { name: "status", field: "status" }, | ||||
|         { name: "type", label: "Type", align: "left", sortable: true }, | ||||
|         { name: "type", label: "Type", field: "action_type", align: "left", sortable: true }, | ||||
|         { name: "due", label: "Due", field: "due", align: "left", sortable: true }, | ||||
|         { name: "desc", label: "Description", align: "left", sortable: true }, | ||||
|         { name: "agent", label: "Agent", align: "left", sortable: true }, | ||||
|         { name: "client", label: "Client", align: "left", sortable: true }, | ||||
|         { name: "site", label: "Site", align: "left", sortable: true }, | ||||
|         { name: "desc", label: "Description", field: "description", align: "left", sortable: true }, | ||||
|         { name: "agent", label: "Agent", field: "hostname", align: "left", sortable: true }, | ||||
|         { name: "client", label: "Client", field: "client", align: "left", sortable: true }, | ||||
|         { name: "site", label: "Site", field: "site", align: "left", sortable: true }, | ||||
|       ], | ||||
|       all_visibleColumns: ["type", "due", "desc", "agent", "client", "site"], | ||||
|       agent_columns: [ | ||||
|         { name: "id", field: "id" }, | ||||
|         { name: "status", field: "status" }, | ||||
|         { name: "type", label: "Type", align: "left", sortable: true }, | ||||
|         { name: "type", label: "Type", field: "action_type", align: "left", sortable: true }, | ||||
|         { name: "due", label: "Due", field: "due", align: "left", sortable: true }, | ||||
|         { name: "desc", label: "Description", align: "left", sortable: true }, | ||||
|         { name: "desc", label: "Description", field: "description", align: "left", sortable: true }, | ||||
|       ], | ||||
|       agent_visibleColumns: ["type", "due", "desc"], | ||||
|     }; | ||||
| @@ -152,6 +152,7 @@ export default { | ||||
|             .then(r => { | ||||
|               this.$q.loading.hide(); | ||||
|               this.getPendingActions(); | ||||
|               this.$emit("edited"); | ||||
|               this.notifySuccess(r.data, 3000); | ||||
|             }) | ||||
|             .catch(e => { | ||||
|   | ||||
| @@ -173,13 +173,13 @@ export default { | ||||
|         timeout: 120, | ||||
|       }, | ||||
|       dayOptions: [ | ||||
|         { label: "Monday", value: 0 }, | ||||
|         { label: "Tuesday", value: 1 }, | ||||
|         { label: "Wednesday", value: 2 }, | ||||
|         { label: "Thursday", value: 3 }, | ||||
|         { label: "Friday", value: 4 }, | ||||
|         { label: "Saturday", value: 5 }, | ||||
|         { label: "Sunday", value: 6 }, | ||||
|         { label: "Monday", value: "Monday" }, | ||||
|         { label: "Tuesday", value: "Tuesday" }, | ||||
|         { label: "Wednesday", value: "Wednesday" }, | ||||
|         { label: "Thursday", value: "Thursday" }, | ||||
|         { label: "Friday", value: "Friday" }, | ||||
|         { label: "Saturday", value: "Saturday" }, | ||||
|         { label: "Sunday", value: "Sunday" }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   | ||||
| @@ -136,6 +136,18 @@ | ||||
|                           <q-item-section>{{ menuMaintenanceText(props.node) }}</q-item-section> | ||||
|                         </q-item> | ||||
|  | ||||
|                         <q-item | ||||
|                           v-if="props.node.children === undefined" | ||||
|                           clickable | ||||
|                           v-close-popup | ||||
|                           @click="showInstallAgent(props.node)" | ||||
|                         > | ||||
|                           <q-item-section side> | ||||
|                             <q-icon name="cloud_download" /> | ||||
|                           </q-item-section> | ||||
|                           <q-item-section>Install Agent</q-item-section> | ||||
|                         </q-item> | ||||
|  | ||||
|                         <q-item clickable v-close-popup @click="showPolicyAdd(props.node)"> | ||||
|                           <q-item-section side> | ||||
|                             <q-icon name="policy" /> | ||||
| @@ -217,6 +229,16 @@ | ||||
|                             </q-item-section> | ||||
|                           </q-item> | ||||
|  | ||||
|                           <q-item> | ||||
|                             <q-item-section side> | ||||
|                               <q-checkbox v-model="filterActionsPending" /> | ||||
|                             </q-item-section> | ||||
|  | ||||
|                             <q-item-section> | ||||
|                               <q-item-label>Actions Pending</q-item-label> | ||||
|                             </q-item-section> | ||||
|                           </q-item> | ||||
|  | ||||
|                           <q-item> | ||||
|                             <q-item-section side> | ||||
|                               <q-checkbox v-model="filterRebootNeeded" /> | ||||
| @@ -328,6 +350,10 @@ | ||||
|     <q-dialog v-model="showPolicyAddModal"> | ||||
|       <PolicyAdd @close="showPolicyAddModal = false" :type="policyAddType" :pk="parseInt(policyAddPk)" /> | ||||
|     </q-dialog> | ||||
|     <!-- add policy modal --> | ||||
|     <q-dialog v-model="showInstallAgentModal" @hide="closeInstallAgent"> | ||||
|       <InstallAgent @close="closeInstallAgent" :sitepk="parseInt(sitePk)" /> | ||||
|     </q-dialog> | ||||
|   </q-layout> | ||||
| </template> | ||||
|  | ||||
| @@ -342,6 +368,7 @@ import AlertsIcon from "@/components/AlertsIcon"; | ||||
| import PolicyAdd from "@/components/automation/modals/PolicyAdd"; | ||||
| import ClientsForm from "@/components/modals/clients/ClientsForm"; | ||||
| import SitesForm from "@/components/modals/clients/SitesForm"; | ||||
| import InstallAgent from "@/components/modals/agents/InstallAgent"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
| @@ -352,6 +379,7 @@ export default { | ||||
|     PolicyAdd, | ||||
|     ClientsForm, | ||||
|     SitesForm, | ||||
|     InstallAgent, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @@ -360,6 +388,8 @@ export default { | ||||
|       showSitesFormModal: false, | ||||
|       showPolicyAddModal: false, | ||||
|       deleteEditModalPk: null, | ||||
|       showInstallAgentModal: false, | ||||
|       sitePk: null, | ||||
|       clientOp: null, | ||||
|       policyAddType: null, | ||||
|       policyAddPk: null, | ||||
| @@ -379,6 +409,7 @@ export default { | ||||
|       filterTextLength: 0, | ||||
|       filterAvailability: "all", | ||||
|       filterPatchesPending: false, | ||||
|       filterActionsPending: false, | ||||
|       filterChecksFailing: false, | ||||
|       filterRebootNeeded: false, | ||||
|       currentTRMMVersion: null, | ||||
| @@ -446,6 +477,12 @@ export default { | ||||
|           align: "left", | ||||
|           sortable: true, | ||||
|         }, | ||||
|         { | ||||
|           name: "pendingactions", | ||||
|           field: "pending_actions", | ||||
|           align: "left", | ||||
|           sortable: true, | ||||
|         }, | ||||
|         { | ||||
|           name: "agentstatus", | ||||
|           field: "status", | ||||
| @@ -483,6 +520,7 @@ export default { | ||||
|         "description", | ||||
|         "user", | ||||
|         "patchespending", | ||||
|         "pendingactions", | ||||
|         "agentstatus", | ||||
|         "needsreboot", | ||||
|         "lastseen", | ||||
| @@ -601,6 +639,14 @@ export default { | ||||
|       this.deleteEditModalPk = null; | ||||
|       this.clientOp = null; | ||||
|     }, | ||||
|     showInstallAgent(node) { | ||||
|       this.sitePk = node.id; | ||||
|       this.showInstallAgentModal = true; | ||||
|     }, | ||||
|     closeInstallAgent() { | ||||
|       this.showInstallAgentModal = false; | ||||
|       this.sitePk = null; | ||||
|     }, | ||||
|     reload() { | ||||
|       this.$store.dispatch("reload"); | ||||
|     }, | ||||
| @@ -655,6 +701,7 @@ export default { | ||||
|       this.filterPatchesPending = false; | ||||
|       this.filterRebootNeeded = false; | ||||
|       this.filterChecksFailing = false; | ||||
|       this.filterActionsPending = false; | ||||
|       this.filterAvailability = "all"; | ||||
|       this.search = ""; | ||||
|     }, | ||||
| @@ -675,6 +722,10 @@ export default { | ||||
|         filterText += "is:patchespending "; | ||||
|       } | ||||
|  | ||||
|       if (this.filterActionsPending) { | ||||
|         filterText += "is:actionspending "; | ||||
|       } | ||||
|  | ||||
|       if (this.filterChecksFailing) { | ||||
|         filterText += "is:checksfailing "; | ||||
|       } | ||||
| @@ -723,6 +774,7 @@ export default { | ||||
|     isFilteringTable() { | ||||
|       return ( | ||||
|         this.filterPatchesPending || | ||||
|         this.filterActionsPending || | ||||
|         this.filterChecksFailing || | ||||
|         this.filterRebootNeeded || | ||||
|         this.filterAvailability !== "all" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user