Compare commits
	
		
			129 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7284d9fcd8 | ||
|  | 515394049a | ||
|  | 35c8b4f535 | ||
|  | 1a325a66b4 | ||
|  | 7d82116fb9 | ||
|  | 8a7bd4f21b | ||
|  | 2e5a2ef12d | ||
|  | 89aceda65a | ||
|  | 39fd83aa16 | ||
|  | a23d811fe8 | ||
|  | a238779724 | ||
|  | 3a848bc037 | ||
|  | 0528ecb454 | ||
|  | 141835593c | ||
|  | 3d06200368 | ||
|  | 729bef9a77 | ||
|  | 94f33bd642 | ||
|  | 7e010cdbca | ||
|  | 8887bcd941 | ||
|  | 56aeeee04c | ||
|  | 98eb3c7287 | ||
|  | 6819c1989b | ||
|  | 7e01dd3e97 | ||
|  | ea4f2c3de8 | ||
|  | b2f63b8761 | ||
|  | 65865101ce | ||
|  | c3637afe69 | ||
|  | ab543ddf0c | ||
|  | 80595e76e7 | ||
|  | d49e68737a | ||
|  | 712e15ba80 | ||
|  | 986160e667 | ||
|  | 1ae4e23db1 | ||
|  | bad646141c | ||
|  | 7911235b68 | ||
|  | 12dee4d14d | ||
|  | cba841beb8 | ||
|  | 4e3ebf7078 | ||
|  | 1c34969f64 | ||
|  | dc26cabacd | ||
|  | a7bffcd471 | ||
|  | 6ae56ac2cc | ||
|  | 03c087020c | ||
|  | 857a1ab9c4 | ||
|  | 64d9530e13 | ||
|  | 5dac1efc30 | ||
|  | 18bc74bc96 | ||
|  | f64efc63f8 | ||
|  | e84b897991 | ||
|  | 519647ef93 | ||
|  | f694fe00e4 | ||
|  | 0b951f27b6 | ||
|  | 8aa082c9df | ||
|  | f2c5d47bd8 | ||
|  | ac7642cc15 | ||
|  | 8f34865dab | ||
|  | c762d12a40 | ||
|  | fe1e71dc07 | ||
|  | 85b0350ed4 | ||
|  | a980491455 | ||
|  | 5798c0ccaa | ||
|  | 742f49ca1f | ||
|  | 5560fc805b | ||
|  | 9d4f8a4e8c | ||
|  | b4d25d6285 | ||
|  | a504a376bd | ||
|  | f61ea6e90a | ||
|  | b2651df36f | ||
|  | b56c086841 | ||
|  | 0b92fee42e | ||
|  | 4343478c7b | ||
|  | 94649cbfc7 | ||
|  | fb83f84d84 | ||
|  | e099a5a32e | ||
|  | 84c2632d40 | ||
|  | 3417ee25eb | ||
|  | 6ada30102c | ||
|  | ac86ca7266 | ||
|  | bb1d3edf71 | ||
|  | 97b9253017 | ||
|  | 971c2180c9 | ||
|  | f96dc6991e | ||
|  | 6855493b2f | ||
|  | ff0d1f7c42 | ||
|  | 3ae5824761 | ||
|  | 702e865715 | ||
|  | 6bcf64c83f | ||
|  | 18b270c9d0 | ||
|  | 783376acb0 | ||
|  | 81dab470d2 | ||
|  | a12f0feb66 | ||
|  | d3c99d9c1c | ||
|  | 3eb3586c0f | ||
|  | fdde16cf56 | ||
|  | b8bc5596fd | ||
|  | 47842a79c7 | ||
|  | 391d5bc386 | ||
|  | ba8561e357 | ||
|  | 6aa1170cef | ||
|  | 6d4363e685 | ||
|  | 6b02b1e1e8 | ||
|  | df3e68fbaf | ||
|  | 58a5550989 | ||
|  | ccc9e44ace | ||
|  | f225c5cf9a | ||
|  | 5c62c7992c | ||
|  | 70b8f09ccb | ||
|  | abfeafa026 | ||
|  | aa029b005f | ||
|  | 6cc55e8f36 | ||
|  | b753d2ca1e | ||
|  | 1e50329c9e | ||
|  | 4942811694 | ||
|  | 59e37e0ccb | ||
|  | 20aa86d8a9 | ||
|  | 64c5ab7042 | ||
|  | d210f5171a | ||
|  | c7eee0f14d | ||
|  | 221753b62e | ||
|  | d213e4d37f | ||
|  | f8695f21d3 | ||
|  | 4ac1030289 | ||
|  | 93c7117319 | ||
|  | 974afd92ce | ||
|  | eafd38d3f2 | ||
|  | c4e590e7a0 | ||
|  | d754f3dd4c | ||
|  | f54fc9e990 | ||
|  | 1bf8ff73f8 | 
| @@ -1,11 +1,11 @@ | ||||
| # pulls community scripts from git repo | ||||
| FROM python:3.11.6-slim AS GET_SCRIPTS_STAGE | ||||
| FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y --no-install-recommends git && \ | ||||
|     git clone https://github.com/amidaware/community-scripts.git /community-scripts | ||||
|  | ||||
| FROM python:3.11.6-slim | ||||
| FROM python:3.11.8-slim | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | ||||
|     name: Tests | ||||
|     strategy: | ||||
|       matrix: | ||||
|         python-version: ["3.11.6"] | ||||
|         python-version: ["3.11.8"] | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|   | ||||
							
								
								
									
										24
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -8,24 +8,6 @@ | ||||
|     "reportGeneralTypeIssues": "none" | ||||
|   }, | ||||
|   "python.analysis.typeCheckingMode": "basic", | ||||
|   "python.linting.enabled": true, | ||||
|   "python.linting.mypyEnabled": true, | ||||
|   "python.linting.mypyArgs": [ | ||||
|     "--ignore-missing-imports", | ||||
|     "--follow-imports=silent", | ||||
|     "--show-column-numbers", | ||||
|     "--strict" | ||||
|   ], | ||||
|   "python.linting.ignorePatterns": [ | ||||
|     "**/site-packages/**/*.py", | ||||
|     ".vscode/*.py", | ||||
|     "**env/**" | ||||
|   ], | ||||
|   "python.formatting.provider": "none", | ||||
|   //"mypy.targets": [ | ||||
|   //"api/tacticalrmm" | ||||
|   //], | ||||
|   //"mypy.runUsingActiveInterpreter": true, | ||||
|   "editor.bracketPairColorization.enabled": true, | ||||
|   "editor.guides.bracketPairs": true, | ||||
|   "editor.formatOnSave": true, | ||||
| @@ -34,7 +16,6 @@ | ||||
|     "**/docker/**/docker-compose*.yml": "dockercompose" | ||||
|   }, | ||||
|   "files.watcherExclude": { | ||||
|     "files.watcherExclude": { | ||||
|       "**/.git/objects/**": true, | ||||
|       "**/.git/subtree-cache/**": true, | ||||
|       "**/node_modules/": true, | ||||
| @@ -53,18 +34,17 @@ | ||||
|       "**/*.parquet*": true, | ||||
|       "**/*.pyc": true, | ||||
|       "**/*.zip": true | ||||
|     } | ||||
|   }, | ||||
|   "go.useLanguageServer": true, | ||||
|   "[go]": { | ||||
|     "editor.codeActionsOnSave": { | ||||
|       "source.organizeImports": false | ||||
|       "source.organizeImports": "never" | ||||
|     }, | ||||
|     "editor.snippetSuggestions": "none" | ||||
|   }, | ||||
|   "[go.mod]": { | ||||
|     "editor.codeActionsOnSave": { | ||||
|       "source.organizeImports": true | ||||
|       "source.organizeImports": "explicit" | ||||
|     } | ||||
|   }, | ||||
|   "gopls": { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas | ||||
| - Teamviewer-like remote desktop control | ||||
| - Real-time remote shell | ||||
| - Remote file browser (download and upload files) | ||||
| - Remote command and script execution (batch, powershell and python scripts) | ||||
| - Remote command and script execution (batch, powershell, python, nushell and deno scripts) | ||||
| - Event log viewer | ||||
| - Services management | ||||
| - Windows patch management | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| --- | ||||
| user: "tactical" | ||||
| python_ver: "3.11.6" | ||||
| python_ver: "3.11.8" | ||||
| go_ver: "1.20.7" | ||||
| backend_repo: "https://github.com/amidaware/tacticalrmm.git" | ||||
| frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git" | ||||
|   | ||||
| @@ -13,7 +13,7 @@ http { | ||||
|         server_tokens off; | ||||
|         tcp_nopush on; | ||||
|         types_hash_max_size 2048; | ||||
|         server_names_hash_bucket_size 64; | ||||
|         server_names_hash_bucket_size 256; | ||||
|         include /etc/nginx/mime.types; | ||||
|         default_type application/octet-stream; | ||||
|         ssl_protocols TLSv1.2 TLSv1.3; | ||||
|   | ||||
| @@ -13,7 +13,6 @@ DATABASES = { | ||||
|         'PORT': '5432', | ||||
|     } | ||||
| } | ||||
| REDIS_HOST    = "localhost" | ||||
| ADMIN_ENABLED = True | ||||
| CERT_FILE = "{{ fullchain_dest }}" | ||||
| KEY_FILE = "{{ privkey_dest }}" | ||||
|   | ||||
| @@ -64,6 +64,15 @@ class User(AbstractUser, BaseAuditModel): | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def mesh_user_id(self): | ||||
|         return f"user//{self.mesh_username}" | ||||
|  | ||||
|     @property | ||||
|     def mesh_username(self): | ||||
|         # lower() needed for mesh api | ||||
|         return f"{self.username.lower()}___{self.pk}" | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(user): | ||||
|         # serializes the task and returns json | ||||
| @@ -195,7 +204,7 @@ class Role(BaseAuditModel): | ||||
|     def save(self, *args, **kwargs) -> None: | ||||
|         # delete cache on save | ||||
|         cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}") | ||||
|         super(BaseAuditModel, self).save(*args, **kwargs) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(role): | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from django.http import HttpRequest | ||||
|  | ||||
|     from accounts.models import User | ||||
|  | ||||
|  | ||||
| @@ -16,3 +18,7 @@ def is_root_user(*, request: "HttpRequest", user: "User") -> bool: | ||||
|         getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER | ||||
|     ) | ||||
|     return root or demo | ||||
|  | ||||
|  | ||||
| def is_superuser(user: "User") -> bool: | ||||
|     return user.role and getattr(user.role, "is_superuser") | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from accounts.utils import is_root_user | ||||
| from core.tasks import sync_mesh_perms_task | ||||
| from logs.models import AuditLog | ||||
| from tacticalrmm.helpers import notify_error | ||||
|  | ||||
| @@ -133,6 +134,7 @@ class GetAddUsers(APIView): | ||||
|             user.role = role | ||||
|  | ||||
|         user.save() | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response(user.username) | ||||
|  | ||||
|  | ||||
| @@ -153,6 +155,7 @@ class GetUpdateDeleteUser(APIView): | ||||
|         serializer = UserSerializer(instance=user, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         sync_mesh_perms_task.delay() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
| @@ -162,7 +165,7 @@ class GetUpdateDeleteUser(APIView): | ||||
|             return notify_error("The root user cannot be deleted from the UI") | ||||
|  | ||||
|         user.delete() | ||||
|  | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| @@ -243,11 +246,13 @@ class GetUpdateDeleteRole(APIView): | ||||
|         serializer = RoleSerializer(instance=role, data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response("Role was edited") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         role.delete() | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response("Role was removed") | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("agents", "0058_alter_agent_time_zone"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="agenthistory", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -20,7 +20,7 @@ from packaging.version import Version as LooseVersion | ||||
| from agents.utils import get_agent_url | ||||
| from checks.models import CheckResult | ||||
| from core.models import TZ_CHOICES | ||||
| from core.utils import get_core_settings, send_command_with_mesh | ||||
| from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh | ||||
| from logs.models import BaseAuditModel, DebugLog, PendingAction | ||||
| from tacticalrmm.constants import ( | ||||
|     AGENT_STATUS_OFFLINE, | ||||
| @@ -126,6 +126,22 @@ class Agent(BaseAuditModel): | ||||
|     def __str__(self) -> str: | ||||
|         return self.hostname | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         # prevent recursion since calling set_alert_template() also calls save() | ||||
|         if not hasattr(self, "_processing_set_alert_template"): | ||||
|             self._processing_set_alert_template = False | ||||
|  | ||||
|         if self.pk and not self._processing_set_alert_template: | ||||
|             orig = Agent.objects.get(pk=self.pk) | ||||
|             mon_type_changed = self.monitoring_type != orig.monitoring_type | ||||
|             site_changed = self.site_id != orig.site_id | ||||
|             if mon_type_changed or site_changed: | ||||
|                 self._processing_set_alert_template = True | ||||
|                 self.set_alert_template() | ||||
|                 self._processing_set_alert_template = False | ||||
|  | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def client(self) -> "Client": | ||||
|         return self.site.client | ||||
| @@ -282,7 +298,20 @@ class Agent(BaseAuditModel): | ||||
|         try: | ||||
|             cpus = self.wmi_detail["cpu"] | ||||
|             for cpu in cpus: | ||||
|                 ret.append([x["Name"] for x in cpu if "Name" in x][0]) | ||||
|                 name = [x["Name"] for x in cpu if "Name" in x][0] | ||||
|                 lp, nc = "", "" | ||||
|                 with suppress(Exception): | ||||
|                     lp = [ | ||||
|                         x["NumberOfLogicalProcessors"] | ||||
|                         for x in cpu | ||||
|                         if "NumberOfCores" in x | ||||
|                     ][0] | ||||
|                     nc = [x["NumberOfCores"] for x in cpu if "NumberOfCores" in x][0] | ||||
|                 if lp and nc: | ||||
|                     cpu_string = f"{name}, {nc}C/{lp}T" | ||||
|                 else: | ||||
|                     cpu_string = name | ||||
|                 ret.append(cpu_string) | ||||
|             return ret | ||||
|         except: | ||||
|             return ["unknown cpu model"] | ||||
| @@ -413,13 +442,20 @@ class Agent(BaseAuditModel): | ||||
|     @property | ||||
|     def serial_number(self) -> str: | ||||
|         if self.is_posix: | ||||
|             return "" | ||||
|             try: | ||||
|                 return self.wmi_detail["serialnumber"] | ||||
|             except: | ||||
|                 return "" | ||||
|  | ||||
|         try: | ||||
|             return self.wmi_detail["bios"][0][0]["SerialNumber"] | ||||
|         except: | ||||
|             return "" | ||||
|  | ||||
|     @property | ||||
|     def hex_mesh_node_id(self) -> str: | ||||
|         return _b64_to_hex(self.mesh_node_id) | ||||
|  | ||||
|     @classmethod | ||||
|     def online_agents(cls, min_version: str = "") -> "List[Agent]": | ||||
|         if min_version: | ||||
| @@ -507,24 +543,32 @@ class Agent(BaseAuditModel): | ||||
|         ) | ||||
|  | ||||
|         return { | ||||
|             "agent_policy": self.policy | ||||
|             if self.policy and not self.policy.is_agent_excluded(self) | ||||
|             else None, | ||||
|             "site_policy": site_policy | ||||
|             if (site_policy and not site_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             else None, | ||||
|             "client_policy": client_policy | ||||
|             if (client_policy and not client_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             and not self.site.block_policy_inheritance | ||||
|             else None, | ||||
|             "default_policy": default_policy | ||||
|             if (default_policy and not default_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             and not self.site.block_policy_inheritance | ||||
|             and not self.client.block_policy_inheritance | ||||
|             else None, | ||||
|             "agent_policy": ( | ||||
|                 self.policy | ||||
|                 if self.policy and not self.policy.is_agent_excluded(self) | ||||
|                 else None | ||||
|             ), | ||||
|             "site_policy": ( | ||||
|                 site_policy | ||||
|                 if (site_policy and not site_policy.is_agent_excluded(self)) | ||||
|                 and not self.block_policy_inheritance | ||||
|                 else None | ||||
|             ), | ||||
|             "client_policy": ( | ||||
|                 client_policy | ||||
|                 if (client_policy and not client_policy.is_agent_excluded(self)) | ||||
|                 and not self.block_policy_inheritance | ||||
|                 and not self.site.block_policy_inheritance | ||||
|                 else None | ||||
|             ), | ||||
|             "default_policy": ( | ||||
|                 default_policy | ||||
|                 if (default_policy and not default_policy.is_agent_excluded(self)) | ||||
|                 and not self.block_policy_inheritance | ||||
|                 and not self.site.block_policy_inheritance | ||||
|                 and not self.client.block_policy_inheritance | ||||
|                 else None | ||||
|             ), | ||||
|         } | ||||
|  | ||||
|     def check_run_interval(self) -> int: | ||||
| @@ -570,6 +614,8 @@ class Agent(BaseAuditModel): | ||||
|             }, | ||||
|             "run_as_user": run_as_user, | ||||
|             "env_vars": parsed_env_vars, | ||||
|             "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|             "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|         } | ||||
|  | ||||
|         if history_pk != 0: | ||||
| @@ -1044,6 +1090,7 @@ class AgentCustomField(models.Model): | ||||
| class AgentHistory(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, | ||||
|         related_name="history", | ||||
|   | ||||
							
								
								
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.constants import AgentMonType | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
|  | ||||
| class AgentSaveTestCase(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.client1 = baker.make("clients.Client") | ||||
|         self.client2 = baker.make("clients.Client") | ||||
|         self.site1 = baker.make("clients.Site", client=self.client1) | ||||
|         self.site2 = baker.make("clients.Site", client=self.client2) | ||||
|         self.site3 = baker.make("clients.Site", client=self.client2) | ||||
|         self.agent = baker.make( | ||||
|             "agents.Agent", | ||||
|             site=self.site1, | ||||
|             monitoring_type=AgentMonType.SERVER, | ||||
|         ) | ||||
|  | ||||
|     @patch.object(Agent, "set_alert_template") | ||||
|     def test_set_alert_template_called_on_mon_type_change( | ||||
|         self, mock_set_alert_template | ||||
|     ): | ||||
|         self.agent.monitoring_type = AgentMonType.WORKSTATION | ||||
|         self.agent.save() | ||||
|         mock_set_alert_template.assert_called_once() | ||||
|  | ||||
|     @patch.object(Agent, "set_alert_template") | ||||
|     def test_set_alert_template_called_on_site_change(self, mock_set_alert_template): | ||||
|         self.agent.site = self.site2 | ||||
|         self.agent.save() | ||||
|         mock_set_alert_template.assert_called_once() | ||||
|  | ||||
|     @patch.object(Agent, "set_alert_template") | ||||
|     def test_set_alert_template_called_on_site_and_montype_change( | ||||
|         self, mock_set_alert_template | ||||
|     ): | ||||
|         print(f"before: {self.agent.monitoring_type} site: {self.agent.site_id}") | ||||
|         self.agent.site = self.site3 | ||||
|         self.agent.monitoring_type = AgentMonType.WORKSTATION | ||||
|         self.agent.save() | ||||
|         mock_set_alert_template.assert_called_once() | ||||
|         print(f"after: {self.agent.monitoring_type} site: {self.agent.site_id}") | ||||
|  | ||||
|     @patch.object(Agent, "set_alert_template") | ||||
|     def test_set_alert_template_not_called_without_changes( | ||||
|         self, mock_set_alert_template | ||||
|     ): | ||||
|         self.agent.save() | ||||
|         mock_set_alert_template.assert_not_called() | ||||
|  | ||||
|     @patch.object(Agent, "set_alert_template") | ||||
|     def test_set_alert_template_not_called_on_non_relevant_field_change( | ||||
|         self, mock_set_alert_template | ||||
|     ): | ||||
|         self.agent.hostname = "abc123" | ||||
|         self.agent.save() | ||||
|         mock_set_alert_template.assert_not_called() | ||||
| @@ -15,6 +15,7 @@ urlpatterns = [ | ||||
|     path("<agent:agent_id>/wmi/", views.WMI.as_view()), | ||||
|     path("<agent:agent_id>/recover/", views.recover), | ||||
|     path("<agent:agent_id>/reboot/", views.Reboot.as_view()), | ||||
|     path("<agent:agent_id>/shutdown/", views.Shutdown.as_view()), | ||||
|     path("<agent:agent_id>/ping/", views.ping), | ||||
|     # alias for checks get view | ||||
|     path("<agent:agent_id>/checks/", GetAddChecks.as_view()), | ||||
|   | ||||
| @@ -21,6 +21,7 @@ from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from core.tasks import sync_mesh_perms_task | ||||
| from core.utils import ( | ||||
|     get_core_settings, | ||||
|     get_mesh_ws_url, | ||||
| @@ -258,6 +259,7 @@ class GetUpdateDeleteAgent(APIView): | ||||
|                     serializer.is_valid(raise_exception=True) | ||||
|                     serializer.save() | ||||
|  | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response("The agent was updated successfully") | ||||
|  | ||||
|     # uninstall agent | ||||
| @@ -283,6 +285,7 @@ class GetUpdateDeleteAgent(APIView): | ||||
|                 message=f"Unable to remove agent {name} from meshcentral database: {e}", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|             ) | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response(f"{name} will now be uninstalled.") | ||||
|  | ||||
|  | ||||
| @@ -325,13 +328,13 @@ class AgentMeshCentral(APIView): | ||||
|         agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|         core = get_core_settings() | ||||
|  | ||||
|         if not core.mesh_disable_auto_login: | ||||
|             token = get_login_token( | ||||
|                 key=core.mesh_token, user=f"user//{core.mesh_username}" | ||||
|             ) | ||||
|             token_param = f"login={token}&" | ||||
|         else: | ||||
|             token_param = "" | ||||
|         user = ( | ||||
|             request.user.mesh_user_id | ||||
|             if core.sync_mesh_with_trmm | ||||
|             else f"user//{core.mesh_api_superuser}" | ||||
|         ) | ||||
|         token = get_login_token(key=core.mesh_token, user=user) | ||||
|         token_param = f"login={token}&" | ||||
|  | ||||
|         control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31" | ||||
|         terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31" | ||||
| @@ -491,6 +494,19 @@ def send_raw_cmd(request, agent_id): | ||||
|     return Response(r) | ||||
|  | ||||
|  | ||||
| class Shutdown(APIView): | ||||
|     permission_classes = [IsAuthenticated, RebootAgentPerms] | ||||
|  | ||||
|     # shutdown | ||||
|     def post(self, request, agent_id): | ||||
|         agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|         r = asyncio.run(agent.nats_cmd({"func": "shutdown"}, timeout=10)) | ||||
|         if r != "ok": | ||||
|             return notify_error("Unable to contact the agent") | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class Reboot(APIView): | ||||
|     permission_classes = [IsAuthenticated, RebootAgentPerms] | ||||
|  | ||||
|   | ||||
| @@ -169,15 +169,17 @@ class Alert(models.Model): | ||||
|                     assigned_check=check, | ||||
|                     agent=agent, | ||||
|                     alert_type=AlertType.CHECK, | ||||
|                     severity=check.alert_severity | ||||
|                     if check.check_type | ||||
|                     not in { | ||||
|                         CheckType.MEMORY, | ||||
|                         CheckType.CPU_LOAD, | ||||
|                         CheckType.DISK_SPACE, | ||||
|                         CheckType.SCRIPT, | ||||
|                     } | ||||
|                     else alert_severity, | ||||
|                     severity=( | ||||
|                         check.alert_severity | ||||
|                         if check.check_type | ||||
|                         not in { | ||||
|                             CheckType.MEMORY, | ||||
|                             CheckType.CPU_LOAD, | ||||
|                             CheckType.DISK_SPACE, | ||||
|                             CheckType.SCRIPT, | ||||
|                         } | ||||
|                         else alert_severity | ||||
|                     ), | ||||
|                     message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.", | ||||
|                     hidden=True, | ||||
|                 ), | ||||
|   | ||||
| @@ -1429,6 +1429,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             "run_as_user": False, | ||||
|             "env_vars": ["hello=world", "foo=bar"], | ||||
|             "id": AgentHistory.objects.last().pk,  # type: ignore | ||||
|             "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|             "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|         } | ||||
|  | ||||
|         nats_cmd.assert_called_with(data, timeout=30, wait=True) | ||||
| @@ -1460,6 +1462,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             "run_as_user": False, | ||||
|             "env_vars": ["resolved=action", "env=vars"], | ||||
|             "id": AgentHistory.objects.last().pk,  # type: ignore | ||||
|             "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|             "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|         } | ||||
|  | ||||
|         nats_cmd.assert_called_with(data, timeout=35, wait=True) | ||||
|   | ||||
| @@ -22,4 +22,12 @@ def get_agent_config() -> AgentCheckInConfig: | ||||
|             *getattr(settings, "CHECKIN_SYNCMESH", (800, 1200)) | ||||
|         ), | ||||
|         limit_data=getattr(settings, "LIMIT_DATA", False), | ||||
|         install_nushell=getattr(settings, "INSTALL_NUSHELL", False), | ||||
|         install_nushell_version=getattr(settings, "INSTALL_NUSHELL_VERSION", ""), | ||||
|         install_nushell_url=getattr(settings, "INSTALL_NUSHELL_URL", ""), | ||||
|         nushell_enable_config=getattr(settings, "NUSHELL_ENABLE_CONFIG", False), | ||||
|         install_deno=getattr(settings, "INSTALL_DENO", False), | ||||
|         install_deno_version=getattr(settings, "INSTALL_DENO_VERSION", ""), | ||||
|         install_deno_url=getattr(settings, "INSTALL_DENO_URL", ""), | ||||
|         deno_default_permissions=getattr(settings, "DENO_DEFAULT_PERMISSIONS", ""), | ||||
|     ) | ||||
|   | ||||
| @@ -20,6 +20,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer | ||||
| from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER | ||||
| from checks.models import Check, CheckResult | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from core.tasks import sync_mesh_perms_task | ||||
| from core.utils import ( | ||||
|     download_mesh_agent, | ||||
|     get_core_settings, | ||||
| @@ -31,6 +32,8 @@ from logs.models import DebugLog, PendingAction | ||||
| from software.models import InstalledSoftware | ||||
| from tacticalrmm.constants import ( | ||||
|     AGENT_DEFER, | ||||
|     TRMM_MAX_REQUEST_SIZE, | ||||
|     AgentHistoryType, | ||||
|     AgentMonType, | ||||
|     AgentPlat, | ||||
|     AuditActionType, | ||||
| @@ -338,6 +341,12 @@ class TaskRunner(APIView): | ||||
|             AutomatedTask.objects.select_related("custom_field"), pk=pk | ||||
|         ) | ||||
|  | ||||
|         content_length = request.META.get("CONTENT_LENGTH") | ||||
|         if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE: | ||||
|             request.data["stdout"] = "" | ||||
|             request.data["stderr"] = "Content truncated due to excessive request size." | ||||
|             request.data["retcode"] = 1 | ||||
|  | ||||
|         # get task result or create if doesn't exist | ||||
|         try: | ||||
|             task_result = ( | ||||
| @@ -356,7 +365,7 @@ class TaskRunner(APIView): | ||||
|  | ||||
|         AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type=AuditActionType.TASK_RUN, | ||||
|             type=AgentHistoryType.TASK_RUN, | ||||
|             command=task.name, | ||||
|             script_results=request.data, | ||||
|         ) | ||||
| @@ -481,6 +490,7 @@ class NewAgent(APIView): | ||||
|         ) | ||||
|  | ||||
|         ret = {"pk": agent.pk, "token": token.key} | ||||
|         sync_mesh_perms_task.delay() | ||||
|         return Response(ret) | ||||
|  | ||||
|  | ||||
| @@ -559,6 +569,15 @@ class AgentHistoryResult(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request, agentid, pk): | ||||
|         content_length = request.META.get("CONTENT_LENGTH") | ||||
|         if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE: | ||||
|  | ||||
|             request.data["script_results"]["stdout"] = "" | ||||
|             request.data["script_results"][ | ||||
|                 "stderr" | ||||
|             ] = "Content truncated due to excessive request size." | ||||
|             request.data["script_results"]["retcode"] = 1 | ||||
|  | ||||
|         hist = get_object_or_404( | ||||
|             AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk | ||||
|         ) | ||||
|   | ||||
| @@ -47,7 +47,7 @@ class Policy(BaseAuditModel): | ||||
|         old_policy: Optional[Policy] = ( | ||||
|             type(self).objects.get(pk=self.pk) if self.pk else None | ||||
|         ) | ||||
|         super(Policy, self).save(old_model=old_policy, *args, **kwargs) | ||||
|         super().save(old_model=old_policy, *args, **kwargs) | ||||
|  | ||||
|         # check if alert template was changes and cache on agents | ||||
|         if old_policy: | ||||
| @@ -68,10 +68,7 @@ class Policy(BaseAuditModel): | ||||
|         cache.delete_many_pattern("site_server_*") | ||||
|         cache.delete_many_pattern("agent_*") | ||||
|  | ||||
|         super(Policy, self).delete( | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().delete(*args, **kwargs) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name | ||||
|   | ||||
| @@ -126,7 +126,7 @@ class TestPolicyViews(TacticalTestCase): | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         cache_alert_template.called_once() | ||||
|         cache_alert_template.assert_called_once() | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("autotasks", "0039_alter_automatedtask_task_type"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="taskresult", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -151,7 +151,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|  | ||||
|         # get old task if exists | ||||
|         old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None | ||||
|         super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs) | ||||
|         super().save(old_model=old_task, *args, **kwargs) | ||||
|  | ||||
|         # check if fields were updated that require a sync to the agent and set status to notsynced | ||||
|         if old_task: | ||||
| @@ -174,10 +174,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             cache.delete_many_pattern("site_*_tasks") | ||||
|             cache.delete_many_pattern("agent_*_tasks") | ||||
|  | ||||
|         super(AutomatedTask, self).delete( | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().delete(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def schedule(self) -> Optional[str]: | ||||
| @@ -248,16 +245,20 @@ class AutomatedTask(BaseAuditModel): | ||||
|             "name": self.win_task_name, | ||||
|             "overwrite_task": True, | ||||
|             "enabled": self.enabled, | ||||
|             "trigger": self.task_type | ||||
|             if self.task_type != TaskType.CHECK_FAILURE | ||||
|             else TaskType.MANUAL, | ||||
|             "trigger": ( | ||||
|                 self.task_type | ||||
|                 if self.task_type != TaskType.CHECK_FAILURE | ||||
|                 else TaskType.MANUAL | ||||
|             ), | ||||
|             "multiple_instances": self.task_instance_policy or 0, | ||||
|             "delete_expired_task_after": self.remove_if_not_scheduled | ||||
|             if self.expire_date | ||||
|             else False, | ||||
|             "start_when_available": self.run_asap_after_missed | ||||
|             if self.task_type != TaskType.RUN_ONCE | ||||
|             else True, | ||||
|             "delete_expired_task_after": ( | ||||
|                 self.remove_if_not_scheduled if self.expire_date else False | ||||
|             ), | ||||
|             "start_when_available": ( | ||||
|                 self.run_asap_after_missed | ||||
|                 if self.task_type != TaskType.RUN_ONCE | ||||
|                 else True | ||||
|             ), | ||||
|         } | ||||
|  | ||||
|         if self.task_type in ( | ||||
| @@ -466,6 +467,7 @@ class TaskResult(models.Model): | ||||
|  | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
|         related_name="taskresults", | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from datetime import datetime | ||||
|  | ||||
| from django.utils import timezone as djangotime | ||||
| from rest_framework import serializers | ||||
| from django.conf import settings | ||||
|  | ||||
| from scripts.models import Script | ||||
| from tacticalrmm.constants import TaskType | ||||
| @@ -257,6 +258,8 @@ class TaskGOGetSerializer(serializers.ModelSerializer): | ||||
|                             shell=script.shell, | ||||
|                             env_vars=env_vars, | ||||
|                         ), | ||||
|                         "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|                         "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|                     } | ||||
|                 ) | ||||
|         if actions_to_remove: | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("checks", "0031_check_env_vars"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="checkhistory", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="checkresult", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -168,10 +168,7 @@ class Check(BaseAuditModel): | ||||
|         elif self.agent: | ||||
|             cache.delete(f"agent_{self.agent.agent_id}_checks") | ||||
|  | ||||
|         super(Check, self).save( | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def delete(self, *args, **kwargs): | ||||
|         # if check is a policy check clear cache on everything | ||||
| @@ -183,10 +180,7 @@ class Check(BaseAuditModel): | ||||
|         elif self.agent: | ||||
|             cache.delete(f"agent_{self.agent.agent_id}_checks") | ||||
|  | ||||
|         super(Check, self).delete( | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().delete(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def readable_desc(self): | ||||
| @@ -290,6 +284,7 @@ class CheckResult(models.Model): | ||||
|     class Meta: | ||||
|         unique_together = (("agent", "assigned_check"),) | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
|         related_name="checkresults", | ||||
| @@ -338,10 +333,7 @@ class CheckResult(models.Model): | ||||
|         ): | ||||
|             self.alert_severity = AlertSeverity.WARNING | ||||
|  | ||||
|         super(CheckResult, self).save( | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def history_info(self): | ||||
| @@ -673,6 +665,7 @@ class CheckResult(models.Model): | ||||
| class CheckHistory(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     check_id = models.PositiveIntegerField(default=0) | ||||
|     agent_id = models.CharField(max_length=200, null=True, blank=True) | ||||
|     x = models.DateTimeField(auto_now_add=True) | ||||
|   | ||||
| @@ -49,11 +49,7 @@ class Client(BaseAuditModel): | ||||
|  | ||||
|         # get old client if exists | ||||
|         old_client = Client.objects.get(pk=self.pk) if self.pk else None | ||||
|         super(Client, self).save( | ||||
|             old_model=old_client, | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().save(old_model=old_client, *args, **kwargs) | ||||
|  | ||||
|         # check if polcies have changed and initiate task to reapply policies if so | ||||
|         if old_client and ( | ||||
| @@ -129,11 +125,7 @@ class Site(BaseAuditModel): | ||||
|  | ||||
|         # get old client if exists | ||||
|         old_site = Site.objects.get(pk=self.pk) if self.pk else None | ||||
|         super(Site, self).save( | ||||
|             old_model=old_site, | ||||
|             *args, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         super().save(old_model=old_site, *args, **kwargs) | ||||
|  | ||||
|         # check if polcies have changed and initiate task to reapply policies if so | ||||
|         if old_site: | ||||
|   | ||||
| @@ -88,6 +88,7 @@ class TestClientViews(TacticalTestCase): | ||||
|             "client": {"name": "Setup Client"}, | ||||
|             "site": {"name": "Setup  Site"}, | ||||
|             "timezone": "America/Los_Angeles", | ||||
|             "companyname": "TestCo Inc.", | ||||
|             "initialsetup": True, | ||||
|         } | ||||
|         r = self.client.post(url, payload, format="json") | ||||
|   | ||||
| @@ -92,7 +92,8 @@ class GetAddClients(APIView): | ||||
|         if "initialsetup" in request.data.keys(): | ||||
|             core = get_core_settings() | ||||
|             core.default_time_zone = request.data["timezone"] | ||||
|             core.save(update_fields=["default_time_zone"]) | ||||
|             core.mesh_company_name = request.data["companyname"] | ||||
|             core.save(update_fields=["default_time_zone", "mesh_company_name"]) | ||||
|  | ||||
|         # save custom fields | ||||
|         if "custom_fields" in request.data.keys(): | ||||
|   | ||||
| @@ -41,6 +41,7 @@ agentBin="${agentBinPath}/${binName}" | ||||
| agentConf='/etc/tacticalagent' | ||||
| agentSvcName='tacticalagent.service' | ||||
| agentSysD="/etc/systemd/system/${agentSvcName}" | ||||
| agentDir='/opt/tacticalagent' | ||||
| meshDir='/opt/tacticalmesh' | ||||
| meshSystemBin="${meshDir}/meshagent" | ||||
| meshSvcName='meshagent.service' | ||||
| @@ -65,16 +66,20 @@ RemoveOldAgent() { | ||||
|     if [ -f "${agentSysD}" ]; then | ||||
|         systemctl disable ${agentSvcName} | ||||
|         systemctl stop ${agentSvcName} | ||||
|         rm -f ${agentSysD} | ||||
|         rm -f "${agentSysD}" | ||||
|         systemctl daemon-reload | ||||
|     fi | ||||
|  | ||||
|     if [ -f "${agentConf}" ]; then | ||||
|         rm -f ${agentConf} | ||||
|         rm -f "${agentConf}" | ||||
|     fi | ||||
|  | ||||
|     if [ -f "${agentBin}" ]; then | ||||
|         rm -f ${agentBin} | ||||
|         rm -f "${agentBin}" | ||||
|     fi | ||||
|  | ||||
|     if [ -d "${agentDir}" ]; then | ||||
|         rm -rf "${agentDir}" | ||||
|     fi | ||||
| } | ||||
|  | ||||
| @@ -132,16 +137,18 @@ Uninstall() { | ||||
|     RemoveOldAgent | ||||
| } | ||||
|  | ||||
| if [ $# -ne 0 ] && [ $1 == 'uninstall' ]; then | ||||
| if [ $# -ne 0 ] && [[ $1 =~ ^(uninstall|-uninstall|--uninstall)$ ]]; then | ||||
|     Uninstall | ||||
|     # Remove the current script | ||||
|     rm "$0" | ||||
|     exit 0 | ||||
| fi | ||||
|  | ||||
| while [[ "$#" -gt 0 ]]; do | ||||
|     case $1 in | ||||
|     --debug) DEBUG=1 ;; | ||||
|     --insecure) INSECURE=1 ;; | ||||
|     --nomesh) NOMESH=1 ;; | ||||
|     -debug | --debug | debug) DEBUG=1 ;; | ||||
|     -insecure | --insecure | insecure) INSECURE=1 ;; | ||||
|     -nomesh | --nomesh | nomesh) NOMESH=1 ;; | ||||
|     *) | ||||
|         echo "ERROR: Unknown parameter: $1" | ||||
|         exit 1 | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class Command(BaseCommand): | ||||
|         self._warning("Mesh device group:", core.mesh_device_group) | ||||
|  | ||||
|         try: | ||||
|             token = get_auth_token(core.mesh_username, core.mesh_token) | ||||
|             token = get_auth_token(core.mesh_api_superuser, core.mesh_token) | ||||
|         except Exception as e: | ||||
|             self._error("Error getting auth token:") | ||||
|             self._error(str(e)) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from tacticalrmm.constants import ( | ||||
|     AGENT_OUTAGES_LOCK, | ||||
|     ORPHANED_WIN_TASK_LOCK, | ||||
|     RESOLVE_ALERTS_LOCK, | ||||
|     SYNC_MESH_PERMS_TASK_LOCK, | ||||
|     SYNC_SCHED_TASK_LOCK, | ||||
| ) | ||||
|  | ||||
| @@ -18,5 +19,6 @@ class Command(BaseCommand): | ||||
|             ORPHANED_WIN_TASK_LOCK, | ||||
|             RESOLVE_ALERTS_LOCK, | ||||
|             SYNC_SCHED_TASK_LOCK, | ||||
|             SYNC_MESH_PERMS_TASK_LOCK, | ||||
|         ): | ||||
|             cache.delete(key) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import os | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from tacticalrmm.helpers import get_nats_internal_protocol, get_nats_ports | ||||
| from tacticalrmm.helpers import get_nats_url | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
| @@ -20,11 +20,9 @@ class Command(BaseCommand): | ||||
|         else: | ||||
|             ssl = "disable" | ||||
|  | ||||
|         nats_std_port, _ = get_nats_ports() | ||||
|         proto = get_nats_internal_protocol() | ||||
|         config = { | ||||
|             "key": settings.SECRET_KEY, | ||||
|             "natsurl": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}", | ||||
|             "natsurl": get_nats_url(), | ||||
|             "user": db["USER"], | ||||
|             "pass": db["PASSWORD"], | ||||
|             "host": db["HOST"], | ||||
|   | ||||
| @@ -5,13 +5,14 @@ import websockets | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.utils import get_mesh_ws_url | ||||
| from tacticalrmm.constants import TRMM_WS_MAX_SIZE | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Sets up initial mesh central configuration" | ||||
|  | ||||
|     async def websocket_call(self, uri): | ||||
|         async with websockets.connect(uri) as websocket: | ||||
|         async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as websocket: | ||||
|             # Get Invitation Link | ||||
|             await websocket.send( | ||||
|                 json.dumps( | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
| from meshctrl.utils import get_login_token | ||||
|  | ||||
| from core.utils import get_core_settings | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "generate a url to login to mesh as the superuser" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         core = get_core_settings() | ||||
|  | ||||
|         token = get_login_token(key=core.mesh_token, user=f"user//{core.mesh_username}") | ||||
|         token_param = f"login={token}&" | ||||
|  | ||||
|         control = f"{core.mesh_site}/?{token_param}" | ||||
|  | ||||
|         self.stdout.write(self.style.SUCCESS(control)) | ||||
| @@ -6,13 +6,14 @@ from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.utils import get_core_settings, get_mesh_ws_url | ||||
| from tacticalrmm.constants import TRMM_WS_MAX_SIZE | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Sets up initial mesh central configuration" | ||||
|  | ||||
|     async def websocket_call(self, uri): | ||||
|         async with websockets.connect(uri) as websocket: | ||||
|         async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as websocket: | ||||
|             # Get Device groups to see if it exists | ||||
|             await websocket.send(json.dumps({"action": "meshes"})) | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,8 @@ from accounts.models import User | ||||
| from agents.models import Agent | ||||
| from autotasks.models import AutomatedTask | ||||
| from checks.models import Check, CheckHistory | ||||
| from core.models import CoreSettings | ||||
| from core.tasks import remove_orphaned_history_results, sync_mesh_perms_task | ||||
| from scripts.models import Script | ||||
| from tacticalrmm.constants import AGENT_DEFER, ScriptType | ||||
|  | ||||
| @@ -54,4 +56,22 @@ class Command(BaseCommand): | ||||
|  | ||||
|                 agent.save(update_fields=["goarch"]) | ||||
|  | ||||
|         self.stdout.write( | ||||
|             self.style.SUCCESS("Checking for orphaned history results...") | ||||
|         ) | ||||
|         count = remove_orphaned_history_results() | ||||
|         if count: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS(f"Removed {count} orphaned history results.") | ||||
|             ) | ||||
|  | ||||
|         core = CoreSettings.objects.first() | ||||
|         if core.sync_mesh_with_trmm: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS( | ||||
|                     "Syncing trmm users/permissions with meshcentral, this might take a long time...please wait..." | ||||
|                 ) | ||||
|             ) | ||||
|             sync_mesh_perms_task() | ||||
|  | ||||
|         self.stdout.write("Post update tasks finished") | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from core.tasks import ( | ||||
|     core_maintenance_tasks, | ||||
|     resolve_alerts_task, | ||||
|     resolve_pending_actions, | ||||
|     sync_mesh_perms_task, | ||||
|     sync_scheduled_tasks, | ||||
| ) | ||||
| from winupdate.tasks import auto_approve_updates_task, check_agent_update_schedule_task | ||||
| @@ -28,3 +29,4 @@ class Command(BaseCommand): | ||||
|         remove_orphaned_win_tasks.delay() | ||||
|         auto_approve_updates_task.delay() | ||||
|         check_agent_update_schedule_task.delay() | ||||
|         sync_mesh_perms_task.delay() | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.tasks import sync_mesh_perms_task | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Sync mesh users/perms with trmm users/perms" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         self.stdout.write( | ||||
|             self.style.SUCCESS( | ||||
|                 "Syncing trmm users/permissions with meshcentral, this might take a long time...please wait..." | ||||
|             ) | ||||
|         ) | ||||
|         sync_mesh_perms_task() | ||||
							
								
								
									
										194
									
								
								api/tacticalrmm/core/mesh_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								api/tacticalrmm/core/mesh_utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | ||||
| import asyncio | ||||
| import json | ||||
| import re | ||||
| import traceback | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| import websockets | ||||
|  | ||||
| from accounts.utils import is_superuser | ||||
| from tacticalrmm.constants import TRMM_WS_MAX_SIZE | ||||
| from tacticalrmm.helpers import make_random_password | ||||
| from tacticalrmm.logger import logger | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from accounts.models import User | ||||
|  | ||||
|  | ||||
| def build_mesh_display_name( | ||||
|     *, first_name: str | None, last_name: str | None, company_name: str | None | ||||
| ) -> str: | ||||
|     ret = "" | ||||
|     if first_name: | ||||
|         ret += first_name | ||||
|  | ||||
|     if last_name: | ||||
|         ret += f" {last_name}" | ||||
|  | ||||
|     if ret and company_name: | ||||
|         ret += f" - {company_name}" | ||||
|     elif company_name: | ||||
|         ret += company_name | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def has_mesh_perms(*, user: "User") -> bool: | ||||
|     if user.is_superuser or is_superuser(user): | ||||
|         return True | ||||
|  | ||||
|     return user.role and getattr(user.role, "can_use_mesh") | ||||
|  | ||||
|  | ||||
| def transform_trmm(obj): | ||||
|     ret = [] | ||||
|     try: | ||||
|         for node in obj: | ||||
|             node_id = node["node_id"] | ||||
|             user_ids = [link["_id"] for link in node["links"]] | ||||
|             ret.append({"node_id": node_id, "user_ids": user_ids}) | ||||
|     except Exception: | ||||
|         logger.debug(traceback.format_exc) | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def transform_mesh(obj): | ||||
|     pattern = re.compile(r".*___\d+") | ||||
|     ret = [] | ||||
|     try: | ||||
|         for _, nodes in obj.items(): | ||||
|             for node in nodes: | ||||
|                 node_id = node["_id"] | ||||
|                 try: | ||||
|                     user_ids = [ | ||||
|                         user_id | ||||
|                         for user_id in node["links"].keys() | ||||
|                         if pattern.match(user_id) | ||||
|                     ] | ||||
|                 except KeyError: | ||||
|                     # will trigger on initial sync cuz no mesh users yet | ||||
|                     # also triggers for invalid agents after sync | ||||
|                     pass | ||||
|                 else: | ||||
|                     ret.append({"node_id": node_id, "user_ids": user_ids}) | ||||
|  | ||||
|     except Exception: | ||||
|         logger.debug(traceback.format_exc) | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| class MeshSync: | ||||
|     def __init__(self, uri: str): | ||||
|         self.uri = uri | ||||
|         self.mesh_users = self.get_trmm_mesh_users()  # full list | ||||
|  | ||||
|     def mesh_action( | ||||
|         self, *, payload: dict[str, Any], wait=True | ||||
|     ) -> dict[str, Any] | None: | ||||
|         async def _do(payload): | ||||
|             async with websockets.connect(self.uri, max_size=TRMM_WS_MAX_SIZE) as ws: | ||||
|                 await ws.send(json.dumps(payload)) | ||||
|                 if wait: | ||||
|                     while 1: | ||||
|                         try: | ||||
|                             message = await asyncio.wait_for(ws.recv(), 120) | ||||
|                             r = json.loads(message) | ||||
|                             if r["action"] == payload["action"]: | ||||
|                                 return r | ||||
|                         except asyncio.TimeoutError: | ||||
|                             logger.error("Timeout reached.") | ||||
|                             return None | ||||
|                 else: | ||||
|                     return None | ||||
|  | ||||
|         payload["responseid"] = "meshctrl" | ||||
|         logger.debug(payload) | ||||
|  | ||||
|         return asyncio.run(_do(payload)) | ||||
|  | ||||
|     def get_unique_mesh_users( | ||||
|         self, trmm_agents_list: list[dict[str, Any]] | ||||
|     ) -> list[str]: | ||||
|         userids = [i["links"] for i in trmm_agents_list] | ||||
|         all_ids = [item["_id"] for sublist in userids for item in sublist] | ||||
|         return list(set(all_ids)) | ||||
|  | ||||
|     def get_trmm_mesh_users(self): | ||||
|         payload = {"action": "users"} | ||||
|         ret = { | ||||
|             i["_id"]: i | ||||
|             for i in self.mesh_action(payload=payload, wait=True)["users"] | ||||
|             if re.search(r".*___\d+", i["_id"]) | ||||
|         } | ||||
|         return ret | ||||
|  | ||||
|     def add_users_to_node(self, *, node_id: str, user_ids: list[str]): | ||||
|  | ||||
|         payload = { | ||||
|             "action": "adddeviceuser", | ||||
|             "nodeid": node_id, | ||||
|             "usernames": [s.replace("user//", "") for s in user_ids], | ||||
|             "rights": 72, | ||||
|             "remove": False, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|  | ||||
|     def delete_users_from_node(self, *, node_id: str, user_ids: list[str]): | ||||
|         payload = { | ||||
|             "action": "adddeviceuser", | ||||
|             "nodeid": node_id, | ||||
|             "userids": user_ids, | ||||
|             "rights": 0, | ||||
|             "remove": True, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|  | ||||
|     def update_mesh_displayname(self, *, user_info: dict[str, Any]) -> None: | ||||
|         payload = { | ||||
|             "action": "edituser", | ||||
|             "id": user_info["_id"], | ||||
|             "realname": user_info["full_name"], | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|  | ||||
|     def add_user_to_mesh(self, *, user_info: dict[str, Any]) -> None: | ||||
|         payload = { | ||||
|             "action": "adduser", | ||||
|             "username": user_info["username"], | ||||
|             "email": user_info["email"], | ||||
|             "pass": make_random_password(len=30), | ||||
|             "resetNextLogin": False, | ||||
|             "randomPassword": False, | ||||
|             "removeEvents": False, | ||||
|             "emailVerified": True, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|         if user_info["full_name"]: | ||||
|             self.update_mesh_displayname(user_info=user_info) | ||||
|  | ||||
|     def delete_user_from_mesh(self, *, mesh_user_id: str) -> None: | ||||
|         payload = { | ||||
|             "action": "deleteuser", | ||||
|             "userid": mesh_user_id, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|  | ||||
|     def add_agent_to_user(self, *, user_id: str, node_id: str) -> None: | ||||
|         payload = { | ||||
|             "action": "adddeviceuser", | ||||
|             "nodeid": node_id, | ||||
|             "userids": [user_id], | ||||
|             "rights": 72, | ||||
|             "remove": False, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
|  | ||||
|     def remove_agent_from_user(self, *, user_id: str, node_id: str) -> None: | ||||
|         payload = { | ||||
|             "action": "adddeviceuser", | ||||
|             "nodeid": node_id, | ||||
|             "userids": [user_id], | ||||
|             "rights": 0, | ||||
|             "remove": True, | ||||
|         } | ||||
|         self.mesh_action(payload=payload, wait=False) | ||||
| @@ -0,0 +1,17 @@ | ||||
| # Generated by Django 4.2.9 on 2024-01-26 00:31 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("core", "0038_alter_coresettings_default_time_zone"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="coresettings", | ||||
|             name="smtp_from_name", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.9 on 2024-01-28 02:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("core", "0039_coresettings_smtp_from_name"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="customfield", | ||||
|             name="hide_in_summary", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								api/tacticalrmm/core/migrations/0041_auto_20240128_0301.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/core/migrations/0041_auto_20240128_0301.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.9 on 2024-01-28 03:01 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| def update_hide_in_summary(apps, schema_editor): | ||||
|     CustomField = apps.get_model("core", "CustomField") | ||||
|     for field in CustomField.objects.filter(hide_in_ui=True): | ||||
|         field.hide_in_summary = True | ||||
|         field.save(update_fields=["hide_in_summary"]) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("core", "0040_customfield_hide_in_summary"), | ||||
|     ] | ||||
|  | ||||
|     operations = [migrations.RunPython(update_hide_in_summary)] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-20 02:51 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("core", "0041_auto_20240128_0301"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="coresettings", | ||||
|             name="mesh_company_name", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-23 19:01 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("core", "0042_coresettings_mesh_company_name"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="coresettings", | ||||
|             name="sync_mesh_with_trmm", | ||||
|             field=models.BooleanField(default=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,17 @@ | ||||
| # Generated by Django 4.2.11 on 2024-03-12 05:23 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("core", "0043_coresettings_sync_mesh_with_trmm"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name="coresettings", | ||||
|             name="mesh_disable_auto_login", | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,5 +1,6 @@ | ||||
| import smtplib | ||||
| from contextlib import suppress | ||||
| from email.headerregistry import Address | ||||
| from email.message import EmailMessage | ||||
| from typing import TYPE_CHECKING, List, Optional, cast | ||||
|  | ||||
| @@ -44,6 +45,7 @@ class CoreSettings(BaseAuditModel): | ||||
|     smtp_from_email = models.CharField( | ||||
|         max_length=255, blank=True, default="from@example.com" | ||||
|     ) | ||||
|     smtp_from_name = models.CharField(max_length=255, null=True, blank=True) | ||||
|     smtp_host = models.CharField(max_length=255, blank=True, default="smtp.gmail.com") | ||||
|     smtp_host_user = models.CharField( | ||||
|         max_length=255, blank=True, default="admin@example.com" | ||||
| @@ -72,7 +74,8 @@ class CoreSettings(BaseAuditModel): | ||||
|     mesh_device_group = models.CharField( | ||||
|         max_length=255, null=True, blank=True, default="TacticalRMM" | ||||
|     ) | ||||
|     mesh_disable_auto_login = models.BooleanField(default=False) | ||||
|     mesh_company_name = models.CharField(max_length=255, null=True, blank=True) | ||||
|     sync_mesh_with_trmm = models.BooleanField(default=True) | ||||
|     agent_auto_update = models.BooleanField(default=True) | ||||
|     workstation_policy = models.ForeignKey( | ||||
|         "automation.Policy", | ||||
| @@ -119,7 +122,7 @@ class CoreSettings(BaseAuditModel): | ||||
|                 self.mesh_token = settings.MESH_TOKEN_KEY | ||||
|  | ||||
|         old_settings = type(self).objects.get(pk=self.pk) if self.pk else None | ||||
|         super(BaseAuditModel, self).save(*args, **kwargs) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         if old_settings: | ||||
|             if ( | ||||
| @@ -144,6 +147,11 @@ class CoreSettings(BaseAuditModel): | ||||
|     def __str__(self) -> str: | ||||
|         return "Global Site Settings" | ||||
|  | ||||
|     @property | ||||
|     def mesh_api_superuser(self) -> str: | ||||
|         # must be lowercase otherwise mesh api breaks | ||||
|         return self.mesh_username.lower() | ||||
|  | ||||
|     @property | ||||
|     def sms_is_configured(self) -> bool: | ||||
|         return all( | ||||
| @@ -207,7 +215,14 @@ class CoreSettings(BaseAuditModel): | ||||
|         try: | ||||
|             msg = EmailMessage() | ||||
|             msg["Subject"] = subject | ||||
|             msg["From"] = from_address | ||||
|  | ||||
|             if self.smtp_from_name: | ||||
|                 msg["From"] = Address( | ||||
|                     display_name=self.smtp_from_name, addr_spec=from_address | ||||
|                 ) | ||||
|             else: | ||||
|                 msg["From"] = from_address | ||||
|  | ||||
|             msg["To"] = email_recipients | ||||
|             msg.set_content(body) | ||||
|  | ||||
| @@ -222,9 +237,16 @@ class CoreSettings(BaseAuditModel): | ||||
|                     server.send_message(msg) | ||||
|                     server.quit() | ||||
|                 else: | ||||
|                     # smtp relay. no auth required | ||||
|                     server.send_message(msg) | ||||
|                     server.quit() | ||||
|                     # gmail smtp relay specific handling. | ||||
|                     if self.smtp_host == "smtp-relay.gmail.com": | ||||
|                         server.ehlo() | ||||
|                         server.starttls() | ||||
|                         server.send_message(msg) | ||||
|                         server.quit() | ||||
|                     else: | ||||
|                         # smtp relay. no auth required | ||||
|                         server.send_message(msg) | ||||
|                         server.quit() | ||||
|  | ||||
|         except Exception as e: | ||||
|             DebugLog.error(message=f"Sending email failed with error: {e}") | ||||
| @@ -298,6 +320,7 @@ class CustomField(BaseAuditModel): | ||||
|         default=list, | ||||
|     ) | ||||
|     hide_in_ui = models.BooleanField(default=False) | ||||
|     hide_in_summary = models.BooleanField(default=False) | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = (("model", "name"),) | ||||
| @@ -348,7 +371,7 @@ class CodeSignToken(models.Model): | ||||
|         if not self.pk and CodeSignToken.objects.exists(): | ||||
|             raise ValidationError("There can only be one CodeSignToken instance") | ||||
|  | ||||
|         super(CodeSignToken, self).save(*args, **kwargs) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def is_valid(self) -> bool: | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| from django.conf import settings | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from tacticalrmm.constants import ALL_TIMEZONES | ||||
| @@ -5,7 +6,19 @@ from tacticalrmm.constants import ALL_TIMEZONES | ||||
| from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction | ||||
|  | ||||
|  | ||||
| class CoreSettingsSerializer(serializers.ModelSerializer): | ||||
| class HostedCoreMixin: | ||||
|     def to_representation(self, instance): | ||||
|         ret = super().to_representation(instance)  # type: ignore | ||||
|         if getattr(settings, "HOSTED", False): | ||||
|             for field in ("mesh_site", "mesh_token", "mesh_username"): | ||||
|                 ret[field] = "n/a" | ||||
|  | ||||
|             ret["sync_mesh_with_trmm"] = True | ||||
|  | ||||
|         return ret | ||||
|  | ||||
|  | ||||
| class CoreSettingsSerializer(HostedCoreMixin, serializers.ModelSerializer): | ||||
|     all_timezones = serializers.SerializerMethodField("all_time_zones") | ||||
|  | ||||
|     def all_time_zones(self, obj): | ||||
| @@ -17,7 +30,7 @@ class CoreSettingsSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|  | ||||
| # for audting | ||||
| class CoreSerializer(serializers.ModelSerializer): | ||||
| class CoreSerializer(HostedCoreMixin, serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = CoreSettings | ||||
|         fields = "__all__" | ||||
|   | ||||
| @@ -1,24 +1,36 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import traceback | ||||
| from contextlib import suppress | ||||
| from time import sleep | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| import nats | ||||
| from django.conf import settings | ||||
| from django.db import transaction | ||||
| from django.db.models import Prefetch | ||||
| from django.db.utils import DatabaseError | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from accounts.models import User | ||||
| from accounts.utils import is_superuser | ||||
| from agents.models import Agent | ||||
| from agents.tasks import clear_faults_task, prune_agent_history | ||||
| from alerts.models import Alert | ||||
| from alerts.tasks import prune_resolved_alerts | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from checks.models import Check, CheckResult | ||||
| from checks.models import Check, CheckHistory, CheckResult | ||||
| from checks.tasks import prune_check_history | ||||
| from clients.models import Client, Site | ||||
| from core.utils import get_core_settings | ||||
| from core.mesh_utils import ( | ||||
|     MeshSync, | ||||
|     build_mesh_display_name, | ||||
|     has_mesh_perms, | ||||
|     transform_mesh, | ||||
|     transform_trmm, | ||||
| ) | ||||
| from core.models import CoreSettings | ||||
| from core.utils import get_core_settings, get_mesh_ws_url | ||||
| from logs.models import PendingAction | ||||
| from logs.tasks import prune_audit_log, prune_debug_log | ||||
| from tacticalrmm.celery import app | ||||
| @@ -27,6 +39,7 @@ from tacticalrmm.constants import ( | ||||
|     AGENT_STATUS_ONLINE, | ||||
|     AGENT_STATUS_OVERDUE, | ||||
|     RESOLVE_ALERTS_LOCK, | ||||
|     SYNC_MESH_PERMS_TASK_LOCK, | ||||
|     SYNC_SCHED_TASK_LOCK, | ||||
|     AlertSeverity, | ||||
|     AlertType, | ||||
| @@ -36,15 +49,34 @@ from tacticalrmm.constants import ( | ||||
|     TaskSyncStatus, | ||||
|     TaskType, | ||||
| ) | ||||
| from tacticalrmm.helpers import setup_nats_options | ||||
| from tacticalrmm.helpers import make_random_password, setup_nats_options | ||||
| from tacticalrmm.logger import logger | ||||
| from tacticalrmm.nats_utils import a_nats_cmd | ||||
| from tacticalrmm.permissions import _has_perm_on_agent | ||||
| from tacticalrmm.utils import redis_lock | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from django.db.models import QuerySet | ||||
|     from nats.aio.client import Client as NATSClient | ||||
|  | ||||
| logger = logging.getLogger("trmm") | ||||
|  | ||||
| def remove_orphaned_history_results() -> int: | ||||
|     try: | ||||
|         with transaction.atomic(): | ||||
|             check_hist_agentids = CheckHistory.objects.values_list( | ||||
|                 "agent_id", flat=True | ||||
|             ).distinct() | ||||
|             current_agentids = set(Agent.objects.values_list("agent_id", flat=True)) | ||||
|             orphaned_agentids = [ | ||||
|                 i for i in check_hist_agentids if i not in current_agentids | ||||
|             ] | ||||
|             count, _ = CheckHistory.objects.filter( | ||||
|                 agent_id__in=orphaned_agentids | ||||
|             ).delete() | ||||
|             return count | ||||
|     except Exception as e: | ||||
|         logger.error(str(e)) | ||||
|         return 0 | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -53,6 +85,8 @@ def core_maintenance_tasks() -> None: | ||||
|         remove_if_not_scheduled=True, expire_date__lt=djangotime.now() | ||||
|     ).delete() | ||||
|  | ||||
|     remove_orphaned_history_results() | ||||
|  | ||||
|     core = get_core_settings() | ||||
|  | ||||
|     # remove old CheckHistory data | ||||
| @@ -361,3 +395,168 @@ def cache_db_fields_task() -> None: | ||||
|         agents = qs.filter(site__client=client) | ||||
|         client.failing_checks = _get_failing_data(agents) | ||||
|         client.save(update_fields=["failing_checks"]) | ||||
|  | ||||
|  | ||||
| @app.task(bind=True) | ||||
| def sync_mesh_perms_task(self): | ||||
|     with redis_lock(SYNC_MESH_PERMS_TASK_LOCK, self.app.oid) as acquired: | ||||
|         if not acquired: | ||||
|             return f"{self.app.oid} still running" | ||||
|  | ||||
|         try: | ||||
|             core = CoreSettings.objects.first() | ||||
|             do_not_sync = not core.sync_mesh_with_trmm | ||||
|             uri = get_mesh_ws_url() | ||||
|             ms = MeshSync(uri) | ||||
|  | ||||
|             if do_not_sync: | ||||
|                 for user in ms.mesh_users: | ||||
|                     ms.delete_user_from_mesh(mesh_user_id=user) | ||||
|  | ||||
|                 return | ||||
|  | ||||
|             company_name = core.mesh_company_name | ||||
|             mnp = {"action": "nodes"} | ||||
|             mesh_nodes_raw = ms.mesh_action(payload=mnp, wait=True)["nodes"] | ||||
|  | ||||
|             users = User.objects.select_related("role").filter( | ||||
|                 agent=None, | ||||
|                 is_installer_user=False, | ||||
|                 is_active=True, | ||||
|                 block_dashboard_login=False, | ||||
|             ) | ||||
|  | ||||
|             trmm_agents_meshnodeids = [ | ||||
|                 f"node//{i.hex_mesh_node_id}" | ||||
|                 for i in Agent.objects.only("mesh_node_id") | ||||
|                 if i.mesh_node_id | ||||
|             ] | ||||
|  | ||||
|             mesh_users_dict = {} | ||||
|             for user in users: | ||||
|                 full_name = build_mesh_display_name( | ||||
|                     first_name=user.first_name, | ||||
|                     last_name=user.last_name, | ||||
|                     company_name=company_name, | ||||
|                 ) | ||||
|  | ||||
|                 # mesh user creation will fail if same email exists for another user | ||||
|                 # make sure that doesn't happen by making a random email | ||||
|                 rand_str1 = make_random_password(len=6) | ||||
|                 rand_str2 = make_random_password(len=5) | ||||
|                 email = f"{user.username}.{rand_str1}@tacticalrmm-do-not-change-{rand_str2}.local" | ||||
|                 mesh_users_dict[user.mesh_user_id] = { | ||||
|                     "_id": user.mesh_user_id, | ||||
|                     "username": user.mesh_username, | ||||
|                     "full_name": full_name, | ||||
|                     "email": email, | ||||
|                 } | ||||
|  | ||||
|             new_trmm_agents = [] | ||||
|             for agent in Agent.objects.defer(*AGENT_DEFER): | ||||
|                 agent_dict = { | ||||
|                     "node_id": f"node//{agent.hex_mesh_node_id}", | ||||
|                     "hostname": agent.hostname, | ||||
|                 } | ||||
|                 tmp: list[dict[str, str]] = [] | ||||
|                 for user in users: | ||||
|                     if not has_mesh_perms(user=user): | ||||
|                         logger.debug(f"No mesh perms for {user} on {agent.hostname}") | ||||
|                         continue | ||||
|  | ||||
|                     if (user.is_superuser or is_superuser(user)) or _has_perm_on_agent( | ||||
|                         user, agent.agent_id | ||||
|                     ): | ||||
|                         tmp.append({"_id": user.mesh_user_id}) | ||||
|  | ||||
|                 agent_dict["links"] = tmp | ||||
|                 new_trmm_agents.append(agent_dict) | ||||
|  | ||||
|             final_trmm = transform_trmm(new_trmm_agents) | ||||
|             final_mesh = transform_mesh(mesh_nodes_raw) | ||||
|  | ||||
|             # delete users first | ||||
|             source_users_global = set() | ||||
|             for item in final_trmm: | ||||
|                 source_users_global.update(item["user_ids"]) | ||||
|  | ||||
|             target_users_global = set() | ||||
|             for item in final_mesh: | ||||
|                 target_users_global.update(item["user_ids"]) | ||||
|  | ||||
|             # identify and create new users | ||||
|             new_users = list(source_users_global - target_users_global) | ||||
|             for user_id in new_users: | ||||
|                 user_info = mesh_users_dict[user_id] | ||||
|                 logger.info(f"Adding new user {user_info['username']} to mesh") | ||||
|                 ms.add_user_to_mesh(user_info=user_info) | ||||
|  | ||||
|             users_to_delete_globally = list(target_users_global - source_users_global) | ||||
|             for user_id in users_to_delete_globally: | ||||
|                 logger.info(f"Deleting {user_id} from mesh") | ||||
|                 ms.delete_user_from_mesh(mesh_user_id=user_id) | ||||
|  | ||||
|             source_map = {item["node_id"]: set(item["user_ids"]) for item in final_trmm} | ||||
|             target_map = {item["node_id"]: set(item["user_ids"]) for item in final_mesh} | ||||
|  | ||||
|             def _get_sleep_after_n_inter(n): | ||||
|                 # {number of agents: chunk size} | ||||
|                 thresholds = {250: 150, 500: 275, 800: 300, 1000: 340} | ||||
|                 for threshold, value in sorted(thresholds.items()): | ||||
|                     if n <= threshold: | ||||
|                         return value | ||||
|  | ||||
|                 return 375 | ||||
|  | ||||
|             iter_count = 0 | ||||
|             sleep_after = _get_sleep_after_n_inter(len(source_map)) | ||||
|  | ||||
|             for node_id, source_users in source_map.items(): | ||||
|                 # skip agents without valid node id | ||||
|                 if node_id not in trmm_agents_meshnodeids: | ||||
|                     continue | ||||
|  | ||||
|                 target_users = target_map.get(node_id, set()) - set( | ||||
|                     users_to_delete_globally | ||||
|                 ) | ||||
|                 source_users_adjusted = source_users - set(users_to_delete_globally) | ||||
|  | ||||
|                 # find users that need to be added or deleted | ||||
|                 users_to_add = list(source_users_adjusted - target_users) | ||||
|                 users_to_delete = list(target_users - source_users_adjusted) | ||||
|  | ||||
|                 if users_to_add or users_to_delete: | ||||
|                     iter_count += 1 | ||||
|  | ||||
|                 if users_to_add: | ||||
|                     logger.info(f"Adding {users_to_add} to {node_id}") | ||||
|                     ms.add_users_to_node(node_id=node_id, user_ids=users_to_add) | ||||
|  | ||||
|                 if users_to_delete: | ||||
|                     logger.info(f"Deleting {users_to_delete} from {node_id}") | ||||
|                     ms.delete_users_from_node(node_id=node_id, user_ids=users_to_delete) | ||||
|  | ||||
|                 if iter_count % sleep_after == 0 and iter_count != 0: | ||||
|                     # mesh is very inefficient with sql, give it time to catch up so we don't crash the system | ||||
|                     logger.info( | ||||
|                         f"Sleeping for 7 seconds after {iter_count} iterations." | ||||
|                     ) | ||||
|                     sleep(7) | ||||
|  | ||||
|             # after all done, see if need to update display name | ||||
|             ms2 = MeshSync(uri) | ||||
|             unique_ids = ms2.get_unique_mesh_users(new_trmm_agents) | ||||
|             for user in unique_ids: | ||||
|                 try: | ||||
|                     mesh_realname = ms2.mesh_users[user]["realname"] | ||||
|                 except KeyError: | ||||
|                     mesh_realname = "" | ||||
|                 trmm_realname = mesh_users_dict[user]["full_name"] | ||||
|                 if mesh_realname != trmm_realname: | ||||
|                     logger.info( | ||||
|                         f"Display names don't match. Updating {user} name from {mesh_realname} to {trmm_realname}" | ||||
|                     ) | ||||
|                     ms2.update_mesh_displayname(user_info=mesh_users_dict[user]) | ||||
|  | ||||
|         except Exception: | ||||
|             logger.debug(traceback.format_exc()) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import os | ||||
| from unittest.mock import patch | ||||
|  | ||||
| import requests | ||||
| @@ -11,16 +12,15 @@ from model_bakery import baker | ||||
| from rest_framework.authtoken.models import Token | ||||
|  | ||||
| # from agents.models import Agent | ||||
| from core.utils import get_core_settings, get_meshagent_url | ||||
| from core.utils import get_core_settings, get_mesh_ws_url, get_meshagent_url | ||||
|  | ||||
| # from logs.models import PendingAction | ||||
| from tacticalrmm.constants import ( | ||||
| from tacticalrmm.constants import (  # PAAction,; PAStatus, | ||||
|     CONFIG_MGMT_CMDS, | ||||
|     CustomFieldModel, | ||||
|     MeshAgentIdent, | ||||
|     # PAAction, | ||||
|     # PAStatus, | ||||
| ) | ||||
| from tacticalrmm.helpers import get_nats_hosts, get_nats_url | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .consumers import DashInfo | ||||
| @@ -110,18 +110,63 @@ class TestCoreTasks(TacticalTestCase): | ||||
|  | ||||
|     def test_edit_coresettings(self): | ||||
|         url = "/core/settings/" | ||||
|  | ||||
|         # setup | ||||
|         baker.make("automation.Policy", _quantity=2) | ||||
|         # test normal request | ||||
|         data = { | ||||
|             "smtp_from_email": "newexample@example.com", | ||||
|             "mesh_token": "New_Mesh_Token", | ||||
|             "mesh_site": "https://mesh.example.com", | ||||
|             "mesh_username": "bob", | ||||
|             "sync_mesh_with_trmm": False, | ||||
|         } | ||||
|         r = self.client.put(url, data) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(get_core_settings().smtp_from_email, data["smtp_from_email"]) | ||||
|         self.assertEqual(get_core_settings().mesh_token, data["mesh_token"]) | ||||
|         core = get_core_settings() | ||||
|         self.assertEqual(core.smtp_from_email, "newexample@example.com") | ||||
|         self.assertEqual(core.mesh_token, "New_Mesh_Token") | ||||
|         self.assertEqual(core.mesh_site, "https://mesh.example.com") | ||||
|         self.assertEqual(core.mesh_username, "bob") | ||||
|         self.assertFalse(core.sync_mesh_with_trmm) | ||||
|  | ||||
|         # test to_representation | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.data["smtp_from_email"], "newexample@example.com") | ||||
|         self.assertEqual(r.data["mesh_token"], "New_Mesh_Token") | ||||
|         self.assertEqual(r.data["mesh_site"], "https://mesh.example.com") | ||||
|         self.assertEqual(r.data["mesh_username"], "bob") | ||||
|         self.assertFalse(r.data["sync_mesh_with_trmm"]) | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|     @override_settings(HOSTED=True) | ||||
|     def test_hosted_edit_coresettings(self): | ||||
|         url = "/core/settings/" | ||||
|         baker.make("automation.Policy", _quantity=2) | ||||
|         data = { | ||||
|             "smtp_from_email": "newexample1@example.com", | ||||
|             "mesh_token": "abc123", | ||||
|             "mesh_site": "https://mesh15534.example.com", | ||||
|             "mesh_username": "jane", | ||||
|             "sync_mesh_with_trmm": False, | ||||
|         } | ||||
|         r = self.client.put(url, data) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         core = get_core_settings() | ||||
|         self.assertEqual(core.smtp_from_email, "newexample1@example.com") | ||||
|         self.assertIn("41410834b8bb4481446027f8", core.mesh_token)  # type: ignore | ||||
|         self.assertTrue(core.sync_mesh_with_trmm) | ||||
|         if "GHACTIONS" in os.environ: | ||||
|             self.assertEqual(core.mesh_site, "https://example.com") | ||||
|             self.assertEqual(core.mesh_username, "pipeline") | ||||
|  | ||||
|         # test to_representation | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.data["smtp_from_email"], "newexample1@example.com") | ||||
|         self.assertEqual(r.data["mesh_token"], "n/a") | ||||
|         self.assertEqual(r.data["mesh_site"], "n/a") | ||||
|         self.assertEqual(r.data["mesh_username"], "n/a") | ||||
|         self.assertTrue(r.data["sync_mesh_with_trmm"]) | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
| @@ -445,6 +490,80 @@ class TestCoreMgmtCommands(TacticalTestCase): | ||||
|             call_command("get_config", cmd) | ||||
|  | ||||
|  | ||||
| class TestNatsUrls(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_standard_install(self): | ||||
|         self.assertEqual(get_nats_url(), "nats://127.0.0.1:4222") | ||||
|  | ||||
|     @override_settings( | ||||
|         NATS_STANDARD_PORT=5000, | ||||
|         USE_NATS_STANDARD=True, | ||||
|         ALLOWED_HOSTS=["api.example.com"], | ||||
|     ) | ||||
|     def test_custom_port_nats_standard(self): | ||||
|         self.assertEqual(get_nats_url(), "tls://api.example.com:5000") | ||||
|  | ||||
|     @override_settings(DOCKER_BUILD=True, ALLOWED_HOSTS=["api.example.com"]) | ||||
|     def test_docker_nats(self): | ||||
|         self.assertEqual(get_nats_url(), "nats://api.example.com:4222") | ||||
|  | ||||
|     @patch.dict("os.environ", {"NATS_CONNECT_HOST": "172.20.4.3"}) | ||||
|     @override_settings(ALLOWED_HOSTS=["api.example.com"]) | ||||
|     def test_custom_connect_host_env(self): | ||||
|         self.assertEqual(get_nats_url(), "nats://172.20.4.3:4222") | ||||
|  | ||||
|     def test_standard_nats_hosts(self): | ||||
|         self.assertEqual(get_nats_hosts(), ("127.0.0.1", "127.0.0.1", "127.0.0.1")) | ||||
|  | ||||
|     @override_settings(DOCKER_BUILD=True, ALLOWED_HOSTS=["api.example.com"]) | ||||
|     def test_docker_nats_hosts(self): | ||||
|         self.assertEqual(get_nats_hosts(), ("0.0.0.0", "0.0.0.0", "api.example.com")) | ||||
|  | ||||
|  | ||||
| class TestMeshWSUrl(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     @patch("core.utils.get_auth_token") | ||||
|     def test_standard_install(self, mock_token): | ||||
|         mock_token.return_value = "abc123" | ||||
|         self.assertEqual( | ||||
|             get_mesh_ws_url(), "ws://127.0.0.1:4430/control.ashx?auth=abc123" | ||||
|         ) | ||||
|  | ||||
|     @patch("core.utils.get_auth_token") | ||||
|     @override_settings(MESH_PORT=8876) | ||||
|     def test_standard_install_custom_port(self, mock_token): | ||||
|         mock_token.return_value = "abc123" | ||||
|         self.assertEqual( | ||||
|             get_mesh_ws_url(), "ws://127.0.0.1:8876/control.ashx?auth=abc123" | ||||
|         ) | ||||
|  | ||||
|     @patch("core.utils.get_auth_token") | ||||
|     @override_settings(DOCKER_BUILD=True, MESH_WS_URL="ws://tactical-meshcentral:4443") | ||||
|     def test_docker_install(self, mock_token): | ||||
|         mock_token.return_value = "abc123" | ||||
|         self.assertEqual( | ||||
|             get_mesh_ws_url(), "ws://tactical-meshcentral:4443/control.ashx?auth=abc123" | ||||
|         ) | ||||
|  | ||||
|     @patch("core.utils.get_auth_token") | ||||
|     @override_settings(USE_EXTERNAL_MESH=True) | ||||
|     def test_external_mesh(self, mock_token): | ||||
|         mock_token.return_value = "abc123" | ||||
|  | ||||
|         from core.models import CoreSettings | ||||
|  | ||||
|         core = CoreSettings.objects.first() | ||||
|         core.mesh_site = "https://mesh.external.com"  # type: ignore | ||||
|         core.save(update_fields=["mesh_site"])  # type: ignore | ||||
|         self.assertEqual( | ||||
|             get_mesh_ws_url(), "wss://mesh.external.com/control.ashx?auth=abc123" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TestCorePermissions(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_client() | ||||
|   | ||||
| @@ -16,6 +16,7 @@ from tacticalrmm.constants import ( | ||||
|     AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX, | ||||
|     CORESETTINGS_CACHE_KEY, | ||||
|     ROLE_CACHE_PREFIX, | ||||
|     TRMM_WS_MAX_SIZE, | ||||
|     AgentPlat, | ||||
|     MeshAgentIdent, | ||||
| ) | ||||
| @@ -83,23 +84,23 @@ def get_core_settings() -> "CoreSettings": | ||||
|  | ||||
| def get_mesh_ws_url() -> str: | ||||
|     core = get_core_settings() | ||||
|     token = get_auth_token(core.mesh_username, core.mesh_token) | ||||
|     token = get_auth_token(core.mesh_api_superuser, core.mesh_token) | ||||
|  | ||||
|     if settings.DOCKER_BUILD: | ||||
|         uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" | ||||
|     else: | ||||
|         if getattr(settings, "TRMM_INSECURE", False): | ||||
|             site = core.mesh_site.replace("https", "ws") | ||||
|             uri = f"{site}:4430/control.ashx?auth={token}" | ||||
|         else: | ||||
|         if getattr(settings, "USE_EXTERNAL_MESH", False): | ||||
|             site = core.mesh_site.replace("https", "wss") | ||||
|             uri = f"{site}/control.ashx?auth={token}" | ||||
|         else: | ||||
|             mesh_port = getattr(settings, "MESH_PORT", 4430) | ||||
|             uri = f"ws://127.0.0.1:{mesh_port}/control.ashx?auth={token}" | ||||
|  | ||||
|     return uri | ||||
|  | ||||
|  | ||||
| async def get_mesh_device_id(uri: str, device_group: str) -> None: | ||||
|     async with websockets.connect(uri) as ws: | ||||
|     async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as ws: | ||||
|         payload = {"action": "meshes", "responseid": "meshctrl"} | ||||
|         await ws.send(json.dumps(payload)) | ||||
|  | ||||
|   | ||||
| @@ -15,12 +15,13 @@ from django.views.decorators.csrf import csrf_exempt | ||||
| from redis import from_url | ||||
| from rest_framework.decorators import api_view, permission_classes | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.permissions import AllowAny, IsAuthenticated | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from core.decorators import monitoring_view | ||||
| from core.tasks import sync_mesh_perms_task | ||||
| from core.utils import get_core_settings, sysd_svc_is_running, token_is_valid | ||||
| from logs.models import AuditLog | ||||
| from tacticalrmm.constants import AuditActionType, PAStatus | ||||
| @@ -56,14 +57,29 @@ class GetEditCoreSettings(APIView): | ||||
|         return Response(CoreSettingsSerializer(settings).data) | ||||
|  | ||||
|     def put(self, request): | ||||
|         data = request.data.copy() | ||||
|  | ||||
|         if getattr(settings, "HOSTED", False): | ||||
|             data.pop("mesh_site") | ||||
|             data.pop("mesh_token") | ||||
|             data.pop("mesh_username") | ||||
|             data["sync_mesh_with_trmm"] = True | ||||
|  | ||||
|         coresettings = CoreSettings.objects.first() | ||||
|         serializer = CoreSettingsSerializer(instance=coresettings, data=request.data) | ||||
|         serializer = CoreSettingsSerializer(instance=coresettings, data=data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         sync_mesh_perms_task.delay() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @permission_classes([AllowAny]) | ||||
| def home(request): | ||||
|     return Response({"status": "ok"}) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| def version(request): | ||||
|     return Response(settings.APP_VER) | ||||
| @@ -91,9 +107,9 @@ def dashboard_info(request): | ||||
|             "show_community_scripts": request.user.show_community_scripts, | ||||
|             "dbl_click_action": request.user.agent_dblclick_action, | ||||
|             "default_agent_tbl_tab": request.user.default_agent_tbl_tab, | ||||
|             "url_action": request.user.url_action.id | ||||
|             if request.user.url_action | ||||
|             else None, | ||||
|             "url_action": ( | ||||
|                 request.user.url_action.id if request.user.url_action else None | ||||
|             ), | ||||
|             "client_tree_sort": request.user.client_tree_sort, | ||||
|             "client_tree_splitter": request.user.client_tree_splitter, | ||||
|             "loading_bar_color": request.user.loading_bar_color, | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from zoneinfo import ZoneInfo | ||||
| import validators | ||||
|  | ||||
|  | ||||
| def as_tz(date_obj, tz, format="%b %d, %I:%M %p"): | ||||
| def as_tz(date_obj, tz, format="%b %d %Y, %I:%M %p"): | ||||
|     return date_obj.astimezone(ZoneInfo(tz)).strftime(format) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ Copyright (c) 2023-present Amidaware Inc. | ||||
| This file is subject to the EE License Agreement. | ||||
| For details, see: https://license.tacticalrmm.com/ee | ||||
| """ | ||||
|  | ||||
| import json | ||||
| from typing import TYPE_CHECKING, Any, Dict, List, Tuple | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ Copyright (c) 2023-present Amidaware Inc. | ||||
| This file is subject to the EE License Agreement. | ||||
| For details, see: https://license.tacticalrmm.com/ee | ||||
| """ | ||||
|  | ||||
| import urllib.parse | ||||
| from time import sleep | ||||
| from typing import Any, Optional | ||||
|   | ||||
| @@ -187,9 +187,11 @@ class TestReportTemplateGenerateView: | ||||
|             template=report_template.template_md, | ||||
|             template_type=report_template.type, | ||||
|             css=report_template.template_css if report_template.template_css else "", | ||||
|             html_template=report_template.template_html.id | ||||
|             if report_template.template_html | ||||
|             else None, | ||||
|             html_template=( | ||||
|                 report_template.template_html.id | ||||
|                 if report_template.template_html | ||||
|                 else None | ||||
|             ), | ||||
|             variables=report_template.template_variables, | ||||
|             dependencies={"client": 1}, | ||||
|         ) | ||||
|   | ||||
| @@ -130,9 +130,9 @@ class GenerateReport(APIView): | ||||
|                 template=template.template_md, | ||||
|                 template_type=template.type, | ||||
|                 css=template.template_css or "", | ||||
|                 html_template=template.template_html.id | ||||
|                 if template.template_html | ||||
|                 else None, | ||||
|                 html_template=( | ||||
|                     template.template_html.id if template.template_html else None | ||||
|                 ), | ||||
|                 variables=template.template_variables, | ||||
|                 dependencies=request.data["dependencies"], | ||||
|             ) | ||||
|   | ||||
| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("logs", "0024_remove_pendingaction_cancelable_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="auditlog", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="debuglog", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="pendingaction", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -26,6 +26,7 @@ def get_debug_level() -> str: | ||||
|  | ||||
|  | ||||
| class AuditLog(models.Model): | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     username = models.CharField(max_length=255) | ||||
|     agent = models.CharField(max_length=255, null=True, blank=True) | ||||
|     agent_id = models.CharField(max_length=255, blank=True, null=True) | ||||
| @@ -47,7 +48,7 @@ class AuditLog(models.Model): | ||||
|                 (self.message[:253] + "..") if len(self.message) > 255 else self.message | ||||
|             ) | ||||
|  | ||||
|         return super(AuditLog, self).save(*args, **kwargs) | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def audit_mesh_session( | ||||
| @@ -258,6 +259,7 @@ class AuditLog(models.Model): | ||||
| class DebugLog(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     entry_time = models.DateTimeField(auto_now_add=True) | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
| @@ -347,6 +349,7 @@ class DebugLog(models.Model): | ||||
| class PendingAction(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
|         related_name="pendingactions", | ||||
| @@ -454,10 +457,10 @@ class BaseAuditModel(models.Model): | ||||
|                         debug_info=get_debug_info(), | ||||
|                     ) | ||||
|  | ||||
|         super(BaseAuditModel, self).save(*args, **kwargs) | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def delete(self, *args, **kwargs) -> Tuple[int, Dict[str, int]]: | ||||
|         super(BaseAuditModel, self).delete(*args, **kwargs) | ||||
|         super().delete(*args, **kwargs) | ||||
|  | ||||
|         username = get_username() | ||||
|         if username: | ||||
|   | ||||
| @@ -152,9 +152,11 @@ class TestAuditViews(TacticalTestCase): | ||||
|             self.assertEqual(resp.status_code, 200) | ||||
|             self.assertEqual( | ||||
|                 len(resp.data["audit_logs"]),  # type:ignore | ||||
|                 pagination["rowsPerPage"] | ||||
|                 if req["count"] > pagination["rowsPerPage"] | ||||
|                 else req["count"], | ||||
|                 ( | ||||
|                     pagination["rowsPerPage"] | ||||
|                     if req["count"] > pagination["rowsPerPage"] | ||||
|                     else req["count"] | ||||
|                 ), | ||||
|             ) | ||||
|             self.assertEqual(resp.data["total"], req["count"])  # type:ignore | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| black | ||||
| daphne==4.0.0 | ||||
| daphne==4.1.0 | ||||
| Werkzeug | ||||
| django-extensions | ||||
| isort | ||||
|   | ||||
| @@ -7,4 +7,4 @@ pytest-xdist | ||||
| pytest-cov | ||||
| refurb | ||||
| flake8 | ||||
| daphne==4.0.0 | ||||
| daphne==4.1.0 | ||||
| @@ -1,46 +1,46 @@ | ||||
| adrf==0.1.2 | ||||
| adrf==0.1.5 | ||||
| asgiref==3.7.2 | ||||
| celery==5.3.5 | ||||
| certifi==2023.11.17 | ||||
| celery==5.3.6 | ||||
| certifi==2024.2.2 | ||||
| cffi==1.16.0 | ||||
| channels==4.0.0 | ||||
| channels_redis==4.1.0 | ||||
| cryptography==41.0.7 | ||||
| Django==4.2.7 | ||||
| channels_redis==4.2.0 | ||||
| cryptography==42.0.5 | ||||
| Django==4.2.11 | ||||
| django-cors-headers==4.3.1 | ||||
| django-filter==23.4 | ||||
| django-filter==24.1 | ||||
| django-rest-knox==4.2.0 | ||||
| djangorestframework==3.14.0 | ||||
| drf-spectacular==0.26.5 | ||||
| hiredis==2.2.3 | ||||
| drf-spectacular==0.27.1 | ||||
| hiredis==2.3.2 | ||||
| meshctrl==0.1.15 | ||||
| msgpack==1.0.7 | ||||
| nats-py==2.6.0 | ||||
| packaging==23.2 | ||||
| psutil==5.9.6 | ||||
| psycopg[binary]==3.1.13 | ||||
| msgpack==1.0.8 | ||||
| nats-py==2.7.2 | ||||
| packaging==24.0 | ||||
| psutil==5.9.8 | ||||
| psycopg[binary]==3.1.18 | ||||
| pycparser==2.21 | ||||
| pycryptodome==3.19.0 | ||||
| pycryptodome==3.20.0 | ||||
| pyotp==2.9.0 | ||||
| pyparsing==3.1.1 | ||||
| python-ipware==2.0.0 | ||||
| pyparsing==3.1.2 | ||||
| python-ipware==2.0.2 | ||||
| qrcode==7.4.2 | ||||
| redis==4.5.5 | ||||
| redis==5.0.3 | ||||
| requests==2.31.0 | ||||
| six==1.16.0 | ||||
| sqlparse==0.4.4 | ||||
| twilio==8.10.2 | ||||
| urllib3==2.1.0 | ||||
| uvicorn[standard]==0.23.2 | ||||
| uWSGI==2.0.22 | ||||
| validators==0.20.0 | ||||
| twilio==8.13.0 | ||||
| urllib3==2.2.1 | ||||
| uvicorn[standard]==0.29.0 | ||||
| uWSGI==2.0.24 | ||||
| validators==0.22.0 | ||||
| vine==5.1.0 | ||||
| websockets==12.0 | ||||
| zipp==3.17.0 | ||||
| pandas==2.1.3 | ||||
| zipp==3.18.1 | ||||
| pandas==2.2.1 | ||||
| kaleido==0.2.1 | ||||
| jinja2==3.1.2 | ||||
| markdown==3.5.1 | ||||
| plotly==5.18.0 | ||||
| weasyprint==60.1 | ||||
| jinja2==3.1.3 | ||||
| markdown==3.5.2 | ||||
| plotly==5.19.0 | ||||
| weasyprint==61.2 | ||||
| ocxsect==0.1.5 | ||||
| @@ -0,0 +1,45 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-22 04:51 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("scripts", "0019_script_env_vars"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="script", | ||||
|             name="shell", | ||||
|             field=models.CharField( | ||||
|                 choices=[ | ||||
|                     ("powershell", "Powershell"), | ||||
|                     ("cmd", "Batch (CMD)"), | ||||
|                     ("python", "Python"), | ||||
|                     ("shell", "Shell"), | ||||
|                     ("nushell", "Nushell"), | ||||
|                     ("deno", "Deno"), | ||||
|                 ], | ||||
|                 default="powershell", | ||||
|                 max_length=100, | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="scriptsnippet", | ||||
|             name="shell", | ||||
|             field=models.CharField( | ||||
|                 choices=[ | ||||
|                     ("powershell", "Powershell"), | ||||
|                     ("cmd", "Batch (CMD)"), | ||||
|                     ("python", "Python"), | ||||
|                     ("shell", "Shell"), | ||||
|                     ("nushell", "Nushell"), | ||||
|                     ("deno", "Deno"), | ||||
|                 ], | ||||
|                 default="powershell", | ||||
|                 max_length=15, | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,5 +1,7 @@ | ||||
| import asyncio | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| from agents.models import Agent, AgentHistory | ||||
| from scripts.models import Script | ||||
| from tacticalrmm.celery import app | ||||
| @@ -78,6 +80,8 @@ def bulk_script_task( | ||||
|             }, | ||||
|             "run_as_user": run_as_user, | ||||
|             "env_vars": script.parse_script_env_vars(agent, script.shell, env_vars), | ||||
|             "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|             "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|         } | ||||
|         tup = (agent.agent_id, data) | ||||
|         items.append(tup) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from rest_framework.decorators import api_view, permission_classes | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from django.conf import settings | ||||
|  | ||||
| from agents.permissions import RunScriptPerms | ||||
| from tacticalrmm.constants import ScriptShell, ScriptType | ||||
| @@ -153,7 +154,7 @@ class TestScript(APIView): | ||||
|         ) | ||||
|  | ||||
|         data = { | ||||
|             "func": "runscript", | ||||
|             "func": "runscriptfull", | ||||
|             "timeout": request.data["timeout"], | ||||
|             "script_args": parsed_args, | ||||
|             "payload": { | ||||
| @@ -162,6 +163,8 @@ class TestScript(APIView): | ||||
|             }, | ||||
|             "run_as_user": request.data["run_as_user"], | ||||
|             "env_vars": parsed_env_vars, | ||||
|             "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG, | ||||
|             "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS, | ||||
|         } | ||||
|  | ||||
|         r = asyncio.run( | ||||
| @@ -190,6 +193,10 @@ def download(request, pk): | ||||
|             ext = ".py" | ||||
|         case ScriptShell.SHELL: | ||||
|             ext = ".sh" | ||||
|         case ScriptShell.NUSHELL: | ||||
|             ext = ".nu" | ||||
|         case ScriptShell.DENO: | ||||
|             ext = ".ts" | ||||
|         case _: | ||||
|             ext = "" | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("software", "0003_delete_chocolog"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="installedsoftware", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -15,6 +15,7 @@ class ChocoSoftware(models.Model): | ||||
| class InstalledSoftware(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey(Agent, on_delete=models.CASCADE) | ||||
|     software = models.JSONField() | ||||
|  | ||||
|   | ||||
| @@ -9,13 +9,15 @@ from django.conf import settings | ||||
|  | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") | ||||
|  | ||||
| app = Celery("tacticalrmm", backend="redis://" + settings.REDIS_HOST, broker="redis://" + settings.REDIS_HOST)  # type: ignore | ||||
| redis_host = f"redis://{settings.REDIS_HOST}" | ||||
| app = Celery("tacticalrmm", backend=redis_host, broker=redis_host) | ||||
| app.accept_content = ["application/json"] | ||||
| app.result_serializer = "json" | ||||
| app.task_serializer = "json" | ||||
| app.conf.task_track_started = True | ||||
| app.conf.worker_proc_alive_timeout = 30 | ||||
| app.conf.worker_max_tasks_per_child = 2 | ||||
| app.conf.broker_connection_retry_on_startup = True | ||||
| app.autodiscover_tasks() | ||||
|  | ||||
| app.conf.beat_schedule = { | ||||
| @@ -33,7 +35,7 @@ app.conf.beat_schedule = { | ||||
|     }, | ||||
|     "remove-orphaned-tasks": { | ||||
|         "task": "autotasks.tasks.remove_orphaned_win_tasks", | ||||
|         "schedule": crontab(minute=50, hour="12"), | ||||
|         "schedule": crontab(minute=50, hour="*/2"), | ||||
|     }, | ||||
|     "agent-outages-task": { | ||||
|         "task": "agents.tasks.agent_outages_task", | ||||
| @@ -55,6 +57,10 @@ app.conf.beat_schedule = { | ||||
|         "task": "core.tasks.sync_scheduled_tasks", | ||||
|         "schedule": crontab(minute="*/2", hour="*"), | ||||
|     }, | ||||
|     "sync-mesh-perms-task": { | ||||
|         "task": "core.tasks.sync_mesh_perms_task", | ||||
|         "schedule": crontab(minute="*/4", hour="*"), | ||||
|     }, | ||||
|     "resolve-pending-actions": { | ||||
|         "task": "core.tasks.resolve_pending_actions", | ||||
|         "schedule": timedelta(seconds=100.0), | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import zoneinfo | ||||
| from enum import Enum | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
|  | ||||
|  | ||||
| @@ -30,6 +31,10 @@ RESOLVE_ALERTS_LOCK = "resolve-alerts-lock-key" | ||||
| SYNC_SCHED_TASK_LOCK = "sync-sched-tasks-lock-key" | ||||
| AGENT_OUTAGES_LOCK = "agent-outages-task-lock-key" | ||||
| ORPHANED_WIN_TASK_LOCK = "orphaned-win-task-lock-key" | ||||
| SYNC_MESH_PERMS_TASK_LOCK = "sync-mesh-perms-lock-key" | ||||
|  | ||||
| TRMM_WS_MAX_SIZE = getattr(settings, "TRMM_WS_MAX_SIZE", 100 * 2**20) | ||||
| TRMM_MAX_REQUEST_SIZE = getattr(settings, "TRMM_MAX_REQUEST_SIZE", 10 * 2**20) | ||||
|  | ||||
|  | ||||
| class GoArch(models.TextChoices): | ||||
| @@ -132,6 +137,8 @@ class ScriptShell(models.TextChoices): | ||||
|     CMD = "cmd", "Batch (CMD)" | ||||
|     PYTHON = "python", "Python" | ||||
|     SHELL = "shell", "Shell" | ||||
|     NUSHELL = "nushell", "Nushell" | ||||
|     DENO = "deno", "Deno" | ||||
|  | ||||
|  | ||||
| class ScriptType(models.TextChoices): | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import os | ||||
| import random | ||||
| import secrets | ||||
| import string | ||||
| @@ -43,10 +44,49 @@ def get_nats_ports() -> tuple[int, int]: | ||||
|  | ||||
|  | ||||
| def get_nats_internal_protocol() -> str: | ||||
|     if getattr(settings, "TRMM_INSECURE", False): | ||||
|         return "nats" | ||||
|     if getattr(settings, "USE_NATS_STANDARD", False): | ||||
|         return "tls" | ||||
|  | ||||
|     return "tls" | ||||
|     return "nats" | ||||
|  | ||||
|  | ||||
| def get_nats_hosts() -> tuple[str, str, str]: | ||||
|     std_bind_host = "0.0.0.0" | ||||
|     ws_bind_host = "0.0.0.0" | ||||
|     connect_host = settings.ALLOWED_HOSTS[0] | ||||
|  | ||||
|     # standard install | ||||
|     if not settings.DOCKER_BUILD and not getattr(settings, "USE_NATS_STANDARD", False): | ||||
|         std_bind_host, ws_bind_host, connect_host = ( | ||||
|             "127.0.0.1", | ||||
|             "127.0.0.1", | ||||
|             "127.0.0.1", | ||||
|         ) | ||||
|  | ||||
|     # allow customizing all nats hosts | ||||
|     if "NATS_STD_BIND_HOST" in os.environ: | ||||
|         std_bind_host = os.getenv("NATS_STD_BIND_HOST") | ||||
|     elif hasattr(settings, "NATS_STD_BIND_HOST"): | ||||
|         std_bind_host = settings.NATS_STD_BIND_HOST | ||||
|  | ||||
|     if "NATS_WS_BIND_HOST" in os.environ: | ||||
|         ws_bind_host = os.getenv("NATS_WS_BIND_HOST") | ||||
|     elif hasattr(settings, "NATS_WS_BIND_HOST"): | ||||
|         ws_bind_host = settings.NATS_WS_BIND_HOST | ||||
|  | ||||
|     if "NATS_CONNECT_HOST" in os.environ: | ||||
|         connect_host = os.getenv("NATS_CONNECT_HOST") | ||||
|     elif hasattr(settings, "NATS_CONNECT_HOST"): | ||||
|         connect_host = settings.NATS_CONNECT_HOST | ||||
|  | ||||
|     return std_bind_host, ws_bind_host, connect_host | ||||
|  | ||||
|  | ||||
| def get_nats_url() -> str: | ||||
|     _, _, connect_host = get_nats_hosts() | ||||
|     proto = get_nats_internal_protocol() | ||||
|     port, _ = get_nats_ports() | ||||
|     return f"{proto}://{connect_host}:{port}" | ||||
|  | ||||
|  | ||||
| def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool: | ||||
| @@ -72,10 +112,8 @@ def rand_range(min: int, max: int) -> float: | ||||
|  | ||||
|  | ||||
| def setup_nats_options() -> dict[str, Any]: | ||||
|     nats_std_port, _ = get_nats_ports() | ||||
|     proto = get_nats_internal_protocol() | ||||
|     opts = { | ||||
|         "servers": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}", | ||||
|         "servers": get_nats_url(), | ||||
|         "user": "tacticalrmm", | ||||
|         "name": "trmm-django", | ||||
|         "password": settings.SECRET_KEY, | ||||
|   | ||||
							
								
								
									
										3
									
								
								api/tacticalrmm/tacticalrmm/logger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								api/tacticalrmm/tacticalrmm/logger.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import logging | ||||
|  | ||||
| logger = logging.getLogger("trmm") | ||||
| @@ -4,6 +4,7 @@ from django.db.models import Q | ||||
| from django.shortcuts import get_object_or_404 | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from accounts.models import User | ||||
| @@ -33,7 +34,10 @@ def _has_perm_on_agent(user: "User", agent_id: str) -> bool: | ||||
|     elif not role: | ||||
|         return False | ||||
|  | ||||
|     agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|     agent = get_object_or_404( | ||||
|         Agent.objects.defer(*AGENT_DEFER).select_related("site__client"), | ||||
|         agent_id=agent_id, | ||||
|     ) | ||||
|     can_view_clients = role.can_view_clients.all() if role else None | ||||
|     can_view_sites = role.can_view_sites.all() if role else None | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import os | ||||
| import sys | ||||
| from contextlib import suppress | ||||
| from datetime import timedelta | ||||
| from pathlib import Path | ||||
| @@ -20,27 +21,70 @@ MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh" | ||||
| AUTH_USER_MODEL = "accounts.User" | ||||
|  | ||||
| # latest release | ||||
| TRMM_VERSION = "0.17.2" | ||||
| TRMM_VERSION = "0.18.0" | ||||
|  | ||||
| # https://github.com/amidaware/tacticalrmm-web | ||||
| WEB_VERSION = "0.101.37" | ||||
| WEB_VERSION = "0.101.43" | ||||
|  | ||||
| # bump this version everytime vue code is changed | ||||
| # to alert user they need to manually refresh their browser | ||||
| APP_VER = "0.0.188" | ||||
| APP_VER = "0.0.191" | ||||
|  | ||||
| # https://github.com/amidaware/rmmagent | ||||
| LATEST_AGENT_VER = "2.6.0" | ||||
| LATEST_AGENT_VER = "2.7.0" | ||||
|  | ||||
| MESH_VER = "1.1.9" | ||||
| MESH_VER = "1.1.21" | ||||
|  | ||||
| NATS_SERVER_VER = "2.10.5" | ||||
| NATS_SERVER_VER = "2.10.12" | ||||
|  | ||||
| # Install Nushell on the agent | ||||
| # https://github.com/nushell/nushell | ||||
| INSTALL_NUSHELL = True | ||||
| # GitHub version to download. The file will be downloaded from GitHub, extracted and installed. | ||||
| # Version to download. If INSTALL_NUSHELL_URL is not provided, the file will be downloaded from GitHub, | ||||
| # extracted and installed. | ||||
| INSTALL_NUSHELL_VERSION = "0.91.0" | ||||
| # URL to download directly. This is expected to be the direct URL, unauthenticated, uncompressed, ready to be installed. | ||||
| # Use {OS}, {ARCH} and {VERSION} to specify the GOOS, GOARCH and INSTALL_NUSHELL_VERSION respectively. | ||||
| # Windows: The ".exe" extension will be added automatically. | ||||
| # Examples: | ||||
| #   https://examplle.com/download/nushell/{OS}/{ARCH}/{VERSION}/nu | ||||
| #   https://examplle.com/download/nushell/nu-{VERSION}-{OS}-{ARCH} | ||||
| INSTALL_NUSHELL_URL = "" | ||||
| # Enable Nushell config on the agent | ||||
| # The default is to not enable the config because it could change how scripts run. | ||||
| # However, disabling the config prevents plugins from being registered. | ||||
| # https://github.com/nushell/nushell/issues/10754 | ||||
| # False: --no-config-file option is added to the command line. | ||||
| # True: --config and --env-config options are added to the command line and point to the Agent's directory. | ||||
| NUSHELL_ENABLE_CONFIG = False | ||||
|  | ||||
| # Install Deno on the agent | ||||
| # https://github.com/denoland/deno | ||||
| INSTALL_DENO = True | ||||
| # Version to download. If INSTALL_DENO_URL is not provided, the file will be downloaded from GitHub, | ||||
| # extracted and installed. | ||||
| INSTALL_DENO_VERSION = "v1.41.3" | ||||
| # URL to download directly. This is expected to be the direct URL, unauthenticated, uncompressed, ready to be installed. | ||||
| # Use {OS}, {ARCH} and {VERSION} to specify the GOOS, GOARCH and INSTALL_DENO_VERSION respectively. | ||||
| # Windows: The ".exe" extension will be added automatically. | ||||
| # Examples: | ||||
| #   https://examplle.com/download/deno/{OS}/{ARCH}/{VERSION}/deno | ||||
| #   https://examplle.com/download/deno/deno-{VERSION}-{OS}-{ARCH} | ||||
| INSTALL_DENO_URL = "" | ||||
| # Default permissions for Deno | ||||
| # Space separated list of permissions as listed in the documentation. | ||||
| # https://docs.deno.com/runtime/manual/basics/permissions#permissions | ||||
| # Examples: | ||||
| #   DENO_DEFAULT_PERMISSIONS = "--allow-sys --allow-net --allow-env" | ||||
| #   DENO_DEFAULT_PERMISSIONS = "--allow-all" | ||||
| DENO_DEFAULT_PERMISSIONS = "--allow-all" | ||||
|  | ||||
| # for the update script, bump when need to recreate venv | ||||
| PIP_VER = "40" | ||||
| PIP_VER = "43" | ||||
|  | ||||
| SETUPTOOLS_VER = "68.2.2" | ||||
| WHEEL_VER = "0.41.3" | ||||
| SETUPTOOLS_VER = "69.2.0" | ||||
| WHEEL_VER = "0.43.0" | ||||
|  | ||||
| AGENT_BASE_URL = "https://agents.tacticalrmm.com" | ||||
|  | ||||
| @@ -71,6 +115,7 @@ HOSTED = False | ||||
| SWAGGER_ENABLED = False | ||||
| REDIS_HOST = "127.0.0.1" | ||||
| TRMM_LOG_LEVEL = "ERROR" | ||||
| TRMM_LOG_TO = "file" | ||||
|  | ||||
| with suppress(ImportError): | ||||
|     from .local_settings import *  # noqa | ||||
| @@ -156,7 +201,7 @@ CACHES = { | ||||
|         "BACKEND": "tacticalrmm.cache.TacticalRedisCache", | ||||
|         "LOCATION": f"redis://{REDIS_HOST}:6379", | ||||
|         "OPTIONS": { | ||||
|             "parser_class": "redis.connection.HiredisParser", | ||||
|             "parser_class": "redis.connection._HiredisParser", | ||||
|             "pool_class": "redis.BlockingConnectionPool", | ||||
|             "db": "10", | ||||
|         }, | ||||
| @@ -240,6 +285,24 @@ def get_log_level() -> str: | ||||
|     return TRMM_LOG_LEVEL | ||||
|  | ||||
|  | ||||
| def configure_logging_handler(): | ||||
|     cfg = { | ||||
|         "level": get_log_level(), | ||||
|         "formatter": "verbose", | ||||
|     } | ||||
|  | ||||
|     log_to = os.getenv("TRMM_LOG_TO", TRMM_LOG_TO) | ||||
|  | ||||
|     if log_to == "stdout": | ||||
|         cfg["class"] = "logging.StreamHandler" | ||||
|         cfg["stream"] = sys.stdout | ||||
|     else: | ||||
|         cfg["class"] = "logging.FileHandler" | ||||
|         cfg["filename"] = os.path.join(LOG_DIR, "trmm_debug.log") | ||||
|  | ||||
|     return cfg | ||||
|  | ||||
|  | ||||
| LOGGING = { | ||||
|     "version": 1, | ||||
|     "disable_existing_loggers": False, | ||||
| @@ -256,12 +319,7 @@ LOGGING = { | ||||
|             "filename": os.path.join(LOG_DIR, "django_debug.log"), | ||||
|             "formatter": "verbose", | ||||
|         }, | ||||
|         "trmm": { | ||||
|             "level": get_log_level(), | ||||
|             "class": "logging.FileHandler", | ||||
|             "filename": os.path.join(LOG_DIR, "trmm_debug.log"), | ||||
|             "formatter": "verbose", | ||||
|         }, | ||||
|         "trmm": configure_logging_handler(), | ||||
|     }, | ||||
|     "loggers": { | ||||
|         "django.request": {"handlers": ["file"], "level": "ERROR", "propagate": True}, | ||||
|   | ||||
| @@ -18,3 +18,11 @@ class AgentCheckInConfig(TRMMStruct): | ||||
|     checkin_wmi: int | ||||
|     checkin_syncmesh: int | ||||
|     limit_data: bool | ||||
|     install_nushell: bool | ||||
|     install_nushell_version: str | ||||
|     install_nushell_url: str | ||||
|     nushell_enable_config: bool | ||||
|     install_deno: bool | ||||
|     install_deno_version: str | ||||
|     install_deno_url: str | ||||
|     deno_default_permissions: str | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from knox import views as knox_views | ||||
| from accounts.views import CheckCreds, LoginView | ||||
| from agents.consumers import SendCMD | ||||
| from core.consumers import DashInfo | ||||
| from core.views import home | ||||
|  | ||||
|  | ||||
| class AgentIDConverter: | ||||
| @@ -20,6 +21,7 @@ class AgentIDConverter: | ||||
| register_converter(AgentIDConverter, "agent") | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("", home), | ||||
|     path("checkcreds/", CheckCreds.as_view()), | ||||
|     path("login/", LoginView.as_view()), | ||||
|     path("logout/", knox_views.LogoutView.as_view()), | ||||
|   | ||||
| @@ -33,6 +33,7 @@ from tacticalrmm.constants import ( | ||||
| ) | ||||
| from tacticalrmm.helpers import ( | ||||
|     get_certs, | ||||
|     get_nats_hosts, | ||||
|     get_nats_internal_protocol, | ||||
|     get_nats_ports, | ||||
|     notify_error, | ||||
| @@ -206,13 +207,16 @@ def reload_nats() -> None: | ||||
|             ) | ||||
|  | ||||
|     cert_file, key_file = get_certs() | ||||
|     nats_std_host, nats_ws_host, _ = get_nats_hosts() | ||||
|     nats_std_port, nats_ws_port = get_nats_ports() | ||||
|  | ||||
|     config = { | ||||
|         "authorization": {"users": users}, | ||||
|         "max_payload": 67108864, | ||||
|         "host": nats_std_host, | ||||
|         "port": nats_std_port,  # internal only | ||||
|         "websocket": { | ||||
|             "host": nats_ws_host, | ||||
|             "port": nats_ws_port, | ||||
|             "no_tls": True,  # TLS is handled by nginx, so not needed here | ||||
|         }, | ||||
| @@ -399,6 +403,17 @@ def replace_arg_db_values( | ||||
|     elif value is True or value is False: | ||||
|         return format_shell_bool(value, shell) | ||||
|  | ||||
|     elif isinstance(value, dict): | ||||
|         return json.dumps(value) | ||||
|  | ||||
|     # return str for everything else | ||||
|     try: | ||||
|         ret = str(value) | ||||
|     except Exception: | ||||
|         ret = "" | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def format_shell_array(value: list[str]) -> str: | ||||
|     temp_string = "" | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-19 05:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("winupdate", "0012_auto_20220227_0554"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="winupdate", | ||||
|             name="id", | ||||
|             field=models.BigAutoField(primary_key=True, serialize=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -39,6 +39,7 @@ SCHEDULE_FREQUENCY_CHOICES = [ | ||||
|  | ||||
|  | ||||
| class WinUpdate(models.Model): | ||||
|     id = models.BigAutoField(primary_key=True) | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, related_name="winupdates", on_delete=models.CASCADE | ||||
|     ) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:16-alpine | ||||
| FROM node:20-alpine | ||||
|  | ||||
| WORKDIR /home/node/app | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM nats:2.10.3-alpine | ||||
| FROM nats:2.10.12-alpine | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # creates python virtual env | ||||
| FROM python:3.11.6-slim AS CREATE_VENV_STAGE | ||||
| FROM python:3.11.8-slim AS CREATE_VENV_STAGE | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
| @@ -21,14 +21,14 @@ RUN apt-get update && \ | ||||
|     pip install --no-cache-dir -r ${TACTICAL_TMP_DIR}/api/requirements.txt | ||||
|  | ||||
| # pulls community scripts from git repo | ||||
| FROM python:3.11.6-slim AS GET_SCRIPTS_STAGE | ||||
| FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y --no-install-recommends git && \ | ||||
|     git clone https://github.com/amidaware/community-scripts.git /community-scripts | ||||
|  | ||||
| # runtime image | ||||
| FROM python:3.11.6-slim | ||||
| FROM python:3.11.8-slim | ||||
|  | ||||
| # set env variables | ||||
| ENV VIRTUAL_ENV /opt/venv | ||||
|   | ||||
| @@ -159,7 +159,7 @@ fi | ||||
|  | ||||
| if [ "$1" = 'tactical-celery' ]; then | ||||
|   check_tactical_ready | ||||
|   celery -A tacticalrmm worker --autoscale=30,5 -l info | ||||
|   celery -A tacticalrmm worker --autoscale=20,2 -l info | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-celerybeat' ]; then | ||||
|   | ||||
| @@ -52,7 +52,7 @@ services: | ||||
|     container_name: trmm-init | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     restart: on-failure | ||||
|     command: [ "tactical-init" ] | ||||
|     command: ["tactical-init"] | ||||
|     environment: | ||||
|       POSTGRES_USER: ${POSTGRES_USER} | ||||
|       POSTGRES_PASS: ${POSTGRES_PASS} | ||||
| @@ -84,8 +84,6 @@ services: | ||||
|     restart: always | ||||
|     environment: | ||||
|       API_HOST: ${API_HOST} | ||||
|     ports: | ||||
|       - "4222:4222" | ||||
|     volumes: | ||||
|       - tactical_data:/opt/tactical | ||||
|     networks: | ||||
| @@ -151,7 +149,7 @@ services: | ||||
|     container_name: trmm-backend | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     user: 1000:1000 | ||||
|     command: [ "tactical-backend" ] | ||||
|     command: ["tactical-backend"] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
| @@ -167,7 +165,7 @@ services: | ||||
|     container_name: trmm-websockets | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     user: 1000:1000 | ||||
|     command: [ "tactical-websockets" ] | ||||
|     command: ["tactical-websockets"] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
| @@ -205,7 +203,7 @@ services: | ||||
|     container_name: trmm-celery | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     user: 1000:1000 | ||||
|     command: [ "tactical-celery" ] | ||||
|     command: ["tactical-celery"] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - redis | ||||
| @@ -222,7 +220,7 @@ services: | ||||
|     container_name: trmm-celerybeat | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     user: 1000:1000 | ||||
|     command: [ "tactical-celerybeat" ] | ||||
|     command: ["tactical-celerybeat"] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
|   | ||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,12 +1,12 @@ | ||||
| module github.com/amidaware/tacticalrmm | ||||
|  | ||||
| go 1.21.4 | ||||
| go 1.21.8 | ||||
|  | ||||
| require ( | ||||
| 	github.com/jmoiron/sqlx v1.3.5 | ||||
| 	github.com/lib/pq v1.10.9 | ||||
| 	github.com/nats-io/nats.go v1.31.0 | ||||
| 	github.com/ugorji/go/codec v1.2.11 | ||||
| 	github.com/nats-io/nats.go v1.34.0 | ||||
| 	github.com/ugorji/go/codec v1.2.12 | ||||
| 	github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 | ||||
| ) | ||||
|  | ||||
| @@ -14,10 +14,10 @@ require github.com/sirupsen/logrus v1.9.3 | ||||
|  | ||||
| require ( | ||||
| 	github.com/klauspost/compress v1.17.2 // indirect | ||||
| 	github.com/nats-io/nkeys v0.4.6 // indirect | ||||
| 	github.com/nats-io/nkeys v0.4.7 // indirect | ||||
| 	github.com/nats-io/nuid v1.0.1 // indirect | ||||
| 	github.com/stretchr/testify v1.7.1 // indirect | ||||
| 	golang.org/x/crypto v0.14.0 // indirect | ||||
| 	golang.org/x/sys v0.13.0 // indirect | ||||
| 	golang.org/x/crypto v0.18.0 // indirect | ||||
| 	golang.org/x/sys v0.16.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										20
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.sum
									
									
									
									
									
								
							| @@ -12,10 +12,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= | ||||
| github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= | ||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= | ||||
| github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= | ||||
| github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= | ||||
| github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= | ||||
| github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk= | ||||
| github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= | ||||
| github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= | ||||
| github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= | ||||
| github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= | ||||
| github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| @@ -26,15 +26,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | ||||
| github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||
| github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 h1:PfOl03o+Y+svWrfXAAu1QWUDePu1yqTq0pf4rpnN8eA= | ||||
| github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8= | ||||
| golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= | ||||
| golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= | ||||
| golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= | ||||
| golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= | ||||
| golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | ||||
| golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
|   | ||||
							
								
								
									
										107
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								install.sh
									
									
									
									
									
								
							| @@ -1,9 +1,12 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| SCRIPT_VERSION="80" | ||||
| SCRIPT_VERSION="83" | ||||
| SCRIPT_URL="https://raw.githubusercontent.com/amidaware/tacticalrmm/master/install.sh" | ||||
|  | ||||
| sudo apt install -y curl wget dirmngr gnupg lsb-release ca-certificates | ||||
| sudo apt install -y software-properties-common | ||||
| sudo apt update | ||||
| sudo apt install -y openssl | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| YELLOW='\033[1;33m' | ||||
| @@ -12,7 +15,7 @@ RED='\033[0;31m' | ||||
| NC='\033[0m' | ||||
|  | ||||
| SCRIPTS_DIR='/opt/trmm-community-scripts' | ||||
| PYTHON_VER='3.11.6' | ||||
| PYTHON_VER='3.11.8' | ||||
| SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py' | ||||
| local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py' | ||||
|  | ||||
| @@ -30,6 +33,11 @@ fi | ||||
|  | ||||
| rm -f $TMP_FILE | ||||
|  | ||||
| if [ -d /rmm/api/tacticalrmm ]; then | ||||
|   echo -ne "${RED}ERROR: Existing trmm installation found. The install script must be run on a clean server.${NC}\n" | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| arch=$(uname -m) | ||||
| if [[ "$arch" != "x86_64" ]] && [[ "$arch" != "aarch64" ]]; then | ||||
|   echo -ne "${RED}ERROR: Only x86_64 and aarch64 is supported, not ${arch}${NC}\n" | ||||
| @@ -113,6 +121,14 @@ print_green() { | ||||
|   printf >&2 "\n" | ||||
| } | ||||
|  | ||||
| print_error() { | ||||
|   printf >&2 "${RED}${1}${NC}\n" | ||||
| } | ||||
|  | ||||
| print_yellow() { | ||||
|   printf >&2 "${YELLOW}${1}${NC}\n" | ||||
| } | ||||
|  | ||||
| cls | ||||
|  | ||||
| while [[ $rmmdomain != *[.]*[.]* ]]; do | ||||
| @@ -138,6 +154,34 @@ while [[ $letsemail != *[@]*[.]* ]]; do | ||||
|   read letsemail | ||||
| done | ||||
|  | ||||
| byocert=false | ||||
| if [[ $* == *--use-own-cert* ]]; then | ||||
|   byocert=true | ||||
| fi | ||||
|  | ||||
| if [[ "$byocert" = true ]]; then | ||||
|   while true; do | ||||
|  | ||||
|     print_yellow "Please enter the full path to your fullchain.pem file:" | ||||
|     read -r fullchain_path | ||||
|     print_yellow "Please enter the full path to your privkey.pem file:" | ||||
|     read -r privkey_path | ||||
|  | ||||
|     if [[ ! -f "$fullchain_path" || ! -f "$privkey_path" ]]; then | ||||
|       print_error "One or both files do not exist. Please try again." | ||||
|       continue | ||||
|     fi | ||||
|  | ||||
|     openssl x509 -in "$fullchain_path" -noout >/dev/null | ||||
|     if [[ $? -ne 0 ]]; then | ||||
|       print_error "ERROR: The provided file is not a valid certificate." | ||||
|       exit 1 | ||||
|     fi | ||||
|  | ||||
|     break | ||||
|   done | ||||
| fi | ||||
|  | ||||
| if grep -q manage_etc_hosts /etc/hosts; then | ||||
|   sudo sed -i '/manage_etc_hosts: true/d' /etc/cloud/cloud.cfg >/dev/null | ||||
|   echo -e "\nmanage_etc_hosts: false" | sudo tee --append /etc/cloud/cloud.cfg >/dev/null | ||||
| @@ -167,10 +211,6 @@ if [[ $* == *--insecure* ]]; then | ||||
|   insecure=true | ||||
| fi | ||||
|  | ||||
| sudo apt install -y software-properties-common | ||||
| sudo apt update | ||||
| sudo apt install -y openssl | ||||
|  | ||||
| if [[ "$insecure" = true ]]; then | ||||
|   print_green 'Generating self-signed cert' | ||||
|   certdir='/etc/ssl/tactical' | ||||
| @@ -183,6 +223,10 @@ if [[ "$insecure" = true ]]; then | ||||
|     -nodes -keyout ${CERT_PRIV_KEY} -out ${CERT_PUB_KEY} -subj "/CN=${rootdomain}" \ | ||||
|     -addext "subjectAltName=DNS:${rootdomain},DNS:*.${rootdomain}" | ||||
|  | ||||
| elif [[ "$byocert" = true ]]; then | ||||
|   CERT_PRIV_KEY=$privkey_path | ||||
|   CERT_PUB_KEY=$fullchain_path | ||||
|   sudo chown ${USER}:${USER} $CERT_PRIV_KEY $CERT_PUB_KEY | ||||
| else | ||||
|   sudo apt install -y certbot | ||||
|   print_green 'Getting wildcard cert' | ||||
| @@ -193,10 +237,9 @@ else | ||||
|   done | ||||
|   CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem | ||||
|   CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem | ||||
|   sudo chown ${USER}:${USER} -R /etc/letsencrypt | ||||
| fi | ||||
|  | ||||
| sudo chown ${USER}:${USER} -R /etc/letsencrypt | ||||
|  | ||||
| print_green 'Installing Nginx' | ||||
|  | ||||
| sudo mkdir -p /etc/apt/keyrings | ||||
| @@ -232,7 +275,7 @@ http { | ||||
|         sendfile on; | ||||
|         tcp_nopush on; | ||||
|         types_hash_max_size 2048; | ||||
|         server_names_hash_bucket_size 64; | ||||
|         server_names_hash_bucket_size 256; | ||||
|         include /etc/nginx/mime.types; | ||||
|         default_type application/octet-stream; | ||||
|         ssl_protocols TLSv1.2 TLSv1.3; | ||||
| @@ -254,7 +297,7 @@ done | ||||
| print_green 'Installing NodeJS' | ||||
|  | ||||
| curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg | ||||
| NODE_MAJOR=18 | ||||
| NODE_MAJOR=20 | ||||
| echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list | ||||
| sudo apt update | ||||
| sudo apt install -y gcc g++ make | ||||
| @@ -295,25 +338,25 @@ done | ||||
|  | ||||
| print_green 'Creating database for trmm' | ||||
|  | ||||
| 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 -u postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}" | ||||
| sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "CREATE DATABASE tacticalrmm" | ||||
| sudo -iu postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'" | ||||
| sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}" | ||||
|  | ||||
| print_green 'Creating database for meshcentral' | ||||
|  | ||||
| sudo -u postgres psql -c "CREATE DATABASE meshcentral" | ||||
| sudo -u postgres psql -c "CREATE USER ${MESHPGUSER} WITH PASSWORD '${MESHPGPWD}'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET client_encoding TO 'utf8'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET timezone TO 'UTC'" | ||||
| sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESHPGUSER}" | ||||
| sudo -u postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESHPGUSER}" | ||||
| sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESHPGUSER}" | ||||
| sudo -iu postgres psql -c "CREATE DATABASE meshcentral" | ||||
| sudo -iu postgres psql -c "CREATE USER ${MESHPGUSER} WITH PASSWORD '${MESHPGPWD}'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET client_encoding TO 'utf8'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET timezone TO 'UTC'" | ||||
| sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESHPGUSER}" | ||||
| sudo -iu postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESHPGUSER}" | ||||
| sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESHPGUSER}" | ||||
|  | ||||
| print_green 'Cloning repos' | ||||
|  | ||||
| @@ -448,7 +491,6 @@ DATABASES = { | ||||
|  | ||||
| MESH_USERNAME = "${meshusername}" | ||||
| MESH_SITE = "https://${meshdomain}" | ||||
| REDIS_HOST    = "localhost" | ||||
| ADMIN_ENABLED = True | ||||
| EOF | ||||
| )" | ||||
| @@ -458,6 +500,16 @@ if [[ "$insecure" = true ]]; then | ||||
|   echo "TRMM_INSECURE = True" | tee --append $local_settings >/dev/null | ||||
| fi | ||||
|  | ||||
| if [[ "$byocert" = true ]]; then | ||||
|   owncerts="$( | ||||
|     cat <<EOF | ||||
| CERT_FILE = "${CERT_PUB_KEY}" | ||||
| KEY_FILE = "${CERT_PRIV_KEY}" | ||||
| EOF | ||||
|   )" | ||||
|   echo "${owncerts}" | tee --append $local_settings >/dev/null | ||||
| fi | ||||
|  | ||||
| if [ "$arch" = "x86_64" ]; then | ||||
|   natsapi='nats-api' | ||||
| else | ||||
| @@ -971,6 +1023,7 @@ cd /rmm/api/tacticalrmm | ||||
| source /rmm/api/env/bin/activate | ||||
| python manage.py initial_db_setup | ||||
| python manage.py reload_nats | ||||
| python manage.py sync_mesh_with_trmm | ||||
| deactivate | ||||
| sudo systemctl start nats.service | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							| @@ -12,7 +12,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	version = "3.5.0" | ||||
| 	version = "3.5.4" | ||||
| 	log     = logrus.New() | ||||
| ) | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -4,7 +4,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	_ "github.com/lib/pq" | ||||
| @@ -20,7 +20,7 @@ func GetConfig(cfg string) (db *sqlx.DB, r DjangoConfig, err error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	jret, _ := ioutil.ReadFile(cfg) | ||||
| 	jret, _ := os.ReadFile(cfg) | ||||
| 	err = json.Unmarshal(jret, &r) | ||||
| 	if err != nil { | ||||
| 		return | ||||
|   | ||||
							
								
								
									
										45
									
								
								restore.sh
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								restore.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| SCRIPT_VERSION="54" | ||||
| SCRIPT_VERSION="57" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh' | ||||
|  | ||||
| sudo apt update | ||||
| @@ -13,7 +13,7 @@ RED='\033[0;31m' | ||||
| NC='\033[0m' | ||||
|  | ||||
| SCRIPTS_DIR='/opt/trmm-community-scripts' | ||||
| PYTHON_VER='3.11.6' | ||||
| PYTHON_VER='3.11.8' | ||||
| SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py' | ||||
|  | ||||
| TMP_FILE=$(mktemp -p "" "rmmrestore_XXXXXXXXXX") | ||||
| @@ -29,6 +29,11 @@ fi | ||||
|  | ||||
| rm -f $TMP_FILE | ||||
|  | ||||
| if [ -d /rmm/api/tacticalrmm ]; then | ||||
|   echo -ne "${RED}ERROR: Existing trmm installation found. The restore script must be run on a clean server, please re-read the docs.${NC}\n" | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| arch=$(uname -m) | ||||
| if [[ "$arch" != "x86_64" ]] && [[ "$arch" != "aarch64" ]]; then | ||||
|   echo -ne "${RED}ERROR: Only x86_64 and aarch64 is supported, not ${arch}${NC}\n" | ||||
| @@ -124,7 +129,7 @@ print_green 'Installing NodeJS' | ||||
|  | ||||
| sudo mkdir -p /etc/apt/keyrings | ||||
| curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg | ||||
| NODE_MAJOR=18 | ||||
| NODE_MAJOR=20 | ||||
| echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list | ||||
| sudo apt update | ||||
| sudo apt install -y gcc g++ make | ||||
| @@ -164,7 +169,7 @@ http { | ||||
|         sendfile on; | ||||
|         tcp_nopush on; | ||||
|         types_hash_max_size 2048; | ||||
|         server_names_hash_bucket_size 64; | ||||
|         server_names_hash_bucket_size 256; | ||||
|         include /etc/nginx/mime.types; | ||||
|         default_type application/octet-stream; | ||||
|         ssl_protocols TLSv1.2 TLSv1.3; | ||||
| @@ -353,14 +358,14 @@ fi | ||||
|  | ||||
| print_green 'Creating MeshCentral DB' | ||||
|  | ||||
| sudo -u postgres psql -c "CREATE DATABASE meshcentral" | ||||
| sudo -u postgres psql -c "CREATE USER ${MESH_POSTGRES_USER} WITH PASSWORD '${MESH_POSTGRES_PW}'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET client_encoding TO 'utf8'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET timezone TO 'UTC'" | ||||
| sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESH_POSTGRES_USER}" | ||||
| sudo -u postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESH_POSTGRES_USER}" | ||||
| sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESH_POSTGRES_USER}" | ||||
| sudo -iu postgres psql -c "CREATE DATABASE meshcentral" | ||||
| sudo -iu postgres psql -c "CREATE USER ${MESH_POSTGRES_USER} WITH PASSWORD '${MESH_POSTGRES_PW}'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET client_encoding TO 'utf8'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET timezone TO 'UTC'" | ||||
| sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESH_POSTGRES_USER}" | ||||
| sudo -iu postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESH_POSTGRES_USER}" | ||||
| sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESH_POSTGRES_USER}" | ||||
|  | ||||
| if [ "$FROM_MONGO" = true ]; then | ||||
|   print_green 'Converting mesh mongo to postgres' | ||||
| @@ -431,14 +436,14 @@ print_green 'Restoring the trmm database' | ||||
| pgusername=$(grep -w USER /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') | ||||
| pgpw=$(grep -w PASSWORD /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') | ||||
|  | ||||
| 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 -u postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}" | ||||
| sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "CREATE DATABASE tacticalrmm" | ||||
| sudo -iu postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'" | ||||
| sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'" | ||||
| sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}" | ||||
| sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}" | ||||
|  | ||||
| gzip -d $tmp_dir/postgres/db*.psql.gz | ||||
| PGPASSWORD=${pgpw} psql -h localhost -U ${pgusername} -d tacticalrmm -f $tmp_dir/postgres/db*.psql | ||||
|   | ||||
							
								
								
									
										85
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								update.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| SCRIPT_VERSION="149" | ||||
| SCRIPT_VERSION="152" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/update.sh' | ||||
| LATEST_SETTINGS_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py' | ||||
| YELLOW='\033[1;33m' | ||||
| @@ -10,8 +10,9 @@ NC='\033[0m' | ||||
| THIS_SCRIPT=$(readlink -f "$0") | ||||
|  | ||||
| SCRIPTS_DIR='/opt/trmm-community-scripts' | ||||
| PYTHON_VER='3.11.6' | ||||
| PYTHON_VER='3.11.8' | ||||
| SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py' | ||||
| local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py' | ||||
|  | ||||
| TMP_FILE=$(mktemp -p "" "rmmupdate_XXXXXXXXXX") | ||||
| curl -s -L "${SCRIPT_URL}" >${TMP_FILE} | ||||
| @@ -186,7 +187,7 @@ if ! [[ $CHECK_NGINX_NOLIMIT ]]; then | ||||
| /' $nginxdefaultconf | ||||
| fi | ||||
|  | ||||
| sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 64;/g' $nginxdefaultconf | ||||
| sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 256;/g' $nginxdefaultconf | ||||
|  | ||||
| if ! sudo nginx -t >/dev/null 2>&1; then | ||||
|   sudo nginx -t | ||||
| @@ -245,8 +246,54 @@ if [ -d ~/.config ]; then | ||||
|   sudo chown -R $USER:$GROUP ~/.config | ||||
| fi | ||||
|  | ||||
| if ! which npm >/dev/null; then | ||||
|   sudo apt install -y npm | ||||
| fi | ||||
|  | ||||
| # older distros still might not have npm after above command, due to recent changes to node apt packages which replaces nodesource with official node | ||||
| # if we still don't have npm, force a switch to nodesource | ||||
| if ! which npm >/dev/null; then | ||||
|   sudo systemctl stop meshcentral | ||||
|   sudo chown ${USER}:${USER} -R /meshcentral | ||||
|   sudo apt remove -y nodejs | ||||
|   sudo rm -rf /usr/lib/node_modules | ||||
|  | ||||
|   curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs | ||||
|   sudo npm install -g npm | ||||
|  | ||||
|   cd /meshcentral | ||||
|   rm -rf node_modules/ package-lock.json | ||||
|   npm install | ||||
|   sudo systemctl start meshcentral | ||||
| fi | ||||
|  | ||||
| sudo npm install -g npm | ||||
|  | ||||
| CURRENT_MESH_VER=$(cd /meshcentral/node_modules/meshcentral && node -p -e "require('./package.json').version") | ||||
| if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]]; then | ||||
|   printf >&2 "${GREEN}Updating meshcentral from ${CURRENT_MESH_VER} to ${LATEST_MESH_VER}${NC}\n" | ||||
|   sudo systemctl stop meshcentral | ||||
|   sudo chown ${USER}:${USER} -R /meshcentral | ||||
|   cd /meshcentral | ||||
|   rm -rf node_modules/ package.json package-lock.json | ||||
|   mesh_pkg="$( | ||||
|     cat <<EOF | ||||
| { | ||||
|   "dependencies": { | ||||
|     "archiver": "5.3.1", | ||||
|     "meshcentral": "${LATEST_MESH_VER}", | ||||
|     "otplib": "10.2.3", | ||||
|     "pg": "8.7.1", | ||||
|     "pgtools": "0.3.2" | ||||
|   } | ||||
| } | ||||
| EOF | ||||
|   )" | ||||
|   echo "${mesh_pkg}" >/meshcentral/package.json | ||||
|   npm install | ||||
|   sudo systemctl start meshcentral | ||||
| fi | ||||
|  | ||||
| # update from main repo | ||||
| cd /rmm | ||||
| git config user.email "admin@example.com" | ||||
| @@ -294,14 +341,14 @@ if ! [[ $CHECK_CELERY_CONFIG ]]; then | ||||
|   sed -i 's/CELERYD_OPTS=.*/CELERYD_OPTS="--time-limit=86400 --autoscale=20,2"/g' /etc/conf.d/celery.conf | ||||
| fi | ||||
|  | ||||
| CHECK_ADMIN_ENABLED=$(grep ADMIN_ENABLED /rmm/api/tacticalrmm/tacticalrmm/local_settings.py) | ||||
| CHECK_ADMIN_ENABLED=$(grep ADMIN_ENABLED $local_settings) | ||||
| if ! [[ $CHECK_ADMIN_ENABLED ]]; then | ||||
|   adminenabled="$( | ||||
|     cat <<EOF | ||||
| ADMIN_ENABLED = False | ||||
| EOF | ||||
|   )" | ||||
|   echo "${adminenabled}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py >/dev/null | ||||
|   echo "${adminenabled}" | tee --append $local_settings >/dev/null | ||||
| fi | ||||
|  | ||||
| if [ "$arch" = "x86_64" ]; then | ||||
| @@ -338,10 +385,13 @@ if [ ! -d /opt/tactical/reporting/schemas ]; then | ||||
|   sudo mkdir /opt/tactical/reporting/schemas | ||||
| fi | ||||
|  | ||||
| sed -i '/^REDIS_HOST/d' $local_settings | ||||
|  | ||||
| sudo chown -R ${USER}:${USER} /opt/tactical | ||||
|  | ||||
| python manage.py pre_update_tasks | ||||
| celery -A tacticalrmm purge -f | ||||
| printf >&2 "${GREEN}Running database migrations (this might take a long time)...${NC}\n" | ||||
| python manage.py migrate | ||||
| python manage.py generate_json_schemas | ||||
| python manage.py delete_tokens | ||||
| @@ -506,30 +556,5 @@ for i in nats nats-api rmm daphne celery celerybeat nginx; do | ||||
|   sudo systemctl start ${i} | ||||
| done | ||||
|  | ||||
| CURRENT_MESH_VER=$(cd /meshcentral/node_modules/meshcentral && node -p -e "require('./package.json').version") | ||||
| if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]]; then | ||||
|   printf >&2 "${GREEN}Updating meshcentral from ${CURRENT_MESH_VER} to ${LATEST_MESH_VER}${NC}\n" | ||||
|   sudo systemctl stop meshcentral | ||||
|   sudo chown ${USER}:${USER} -R /meshcentral | ||||
|   cd /meshcentral | ||||
|   rm -rf node_modules/ package.json package-lock.json | ||||
|   mesh_pkg="$( | ||||
|     cat <<EOF | ||||
| { | ||||
|   "dependencies": { | ||||
|     "archiver": "5.3.1", | ||||
|     "meshcentral": "${LATEST_MESH_VER}", | ||||
|     "otplib": "10.2.3", | ||||
|     "pg": "8.7.1", | ||||
|     "pgtools": "0.3.2" | ||||
|   } | ||||
| } | ||||
| EOF | ||||
|   )" | ||||
|   echo "${mesh_pkg}" >/meshcentral/package.json | ||||
|   npm install | ||||
|   sudo systemctl start meshcentral | ||||
| fi | ||||
|  | ||||
| rm -f $TMP_SETTINGS | ||||
| printf >&2 "${GREEN}Update finished!${NC}\n" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user