diff --git a/README.md b/README.md index a7e6dbf9..587b6214 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\ -It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral) +It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) # [LIVE DEMO](https://rmm.xlawgaming.com/) Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app. @@ -62,7 +62,6 @@ sudo ufw default allow outgoing sudo ufw allow ssh sudo ufw allow http sudo ufw allow https -sudo ufw allow proto tcp from any to any port 4505,4506 sudo ufw allow proto tcp from any to any port 4222 sudo ufw enable && sudo ufw reload ``` diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 6fef0d07..611cdec4 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -9,6 +9,7 @@ import validators import msgpack import re from collections import Counter +from typing import List from loguru import logger from packaging import version as pyver from distutils.version import LooseVersion @@ -382,6 +383,13 @@ class Agent(BaseAuditModel): return patch_policy + def get_approved_update_guids(self) -> List[str]: + return list( + self.winupdates.filter(action="approve", installed=False).values_list( + "guid", flat=True + ) + ) + def generate_checks_from_policies(self): from automation.models import Policy @@ -452,77 +460,6 @@ class Agent(BaseAuditModel): await nc.flush() await nc.close() - def salt_api_cmd(self, **kwargs): - - # salt should always timeout first before the requests' timeout - try: - timeout = kwargs["timeout"] - except KeyError: - # default timeout - timeout = 15 - salt_timeout = 12 - else: - if timeout < 8: - timeout = 8 - salt_timeout = 5 - else: - salt_timeout = timeout - 3 - - json = { - "client": "local", - "tgt": self.salt_id, - "fun": kwargs["func"], - "timeout": salt_timeout, - "username": settings.SALT_USERNAME, - "password": settings.SALT_PASSWORD, - "eauth": "pam", - } - - if "arg" in kwargs: - json.update({"arg": kwargs["arg"]}) - if "kwargs" in kwargs: - json.update({"kwarg": kwargs["kwargs"]}) - - try: - resp = requests.post( - f"http://{settings.SALT_HOST}:8123/run", - json=[json], - timeout=timeout, - ) - except Exception: - return "timeout" - - try: - ret = resp.json()["return"][0][self.salt_id] - except Exception as e: - logger.error(f"{self.salt_id}: {e}") - return "error" - else: - return ret - - def salt_api_async(self, **kwargs): - - json = { - "client": "local_async", - "tgt": self.salt_id, - "fun": kwargs["func"], - "username": settings.SALT_USERNAME, - "password": settings.SALT_PASSWORD, - "eauth": "pam", - } - - if "arg" in kwargs: - json.update({"arg": kwargs["arg"]}) - if "kwargs" in kwargs: - json.update({"kwarg": kwargs["kwargs"]}) - - try: - resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json]) - except Exception: - return "timeout" - - return resp - @staticmethod def serialize(agent): # serializes the agent and returns json @@ -533,32 +470,6 @@ class Agent(BaseAuditModel): del ret["client"] return ret - @staticmethod - def salt_batch_async(**kwargs): - assert isinstance(kwargs["minions"], list) - - json = { - "client": "local_async", - "tgt_type": "list", - "tgt": kwargs["minions"], - "fun": kwargs["func"], - "username": settings.SALT_USERNAME, - "password": settings.SALT_PASSWORD, - "eauth": "pam", - } - - if "arg" in kwargs: - json.update({"arg": kwargs["arg"]}) - if "kwargs" in kwargs: - json.update({"kwarg": kwargs["kwargs"]}) - - try: - resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json]) - except Exception: - return "timeout" - - return resp - def delete_superseded_updates(self): try: pks = [] # list of pks to delete diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index b29a4b76..b5a0a599 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -177,29 +177,6 @@ def sync_sysinfo_task(): sleep(rand) -@app.task -def sync_salt_modules_task(pk): - agent = Agent.objects.get(pk=pk) - r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules") - # successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]} - # successful sync with no new/changed files: {'return': [{'MINION-15': []}]} - if r == "timeout" or r == "error": - return f"Unable to sync modules {agent.salt_id}" - - return f"Successfully synced salt modules on {agent.hostname}" - - -@app.task -def batch_sync_modules_task(): - # sync modules, split into chunks of 50 agents to not overload salt - agents = Agent.objects.all() - online = [i.salt_id for i in agents] - chunks = (online[i : i + 50] for i in range(0, len(online), 50)) - for chunk in chunks: - Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules") - sleep(10) - - @app.task def uninstall_agent_task(salt_id, has_nats): attempts = 0 diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index 1af36384..7e3b1f70 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -14,12 +14,6 @@ from tacticalrmm.test import TacticalTestCase from .serializers import AgentSerializer from winupdate.serializers import WinUpdatePolicySerializer from .models import Agent -from .tasks import ( - agent_recovery_sms_task, - auto_self_agent_update_task, - sync_salt_modules_task, - batch_sync_modules_task, -) from winupdate.models import WinUpdatePolicy @@ -543,7 +537,7 @@ class TestAgentViews(TacticalTestCase): self.check_not_authenticated("get", url) - @patch("winupdate.tasks.bulk_check_for_updates_task.delay") + """ @patch("winupdate.tasks.bulk_check_for_updates_task.delay") @patch("scripts.tasks.handle_bulk_script_task.delay") @patch("scripts.tasks.handle_bulk_command_task.delay") @patch("agents.models.Agent.salt_batch_async") @@ -585,7 +579,7 @@ class TestAgentViews(TacticalTestCase): r = self.client.post(url, payload, format="json") self.assertEqual(r.status_code, 400) - """ payload = { + payload = { "mode": "command", "monType": "workstations", "target": "client", @@ -599,7 +593,7 @@ class TestAgentViews(TacticalTestCase): r = self.client.post(url, payload, format="json") self.assertEqual(r.status_code, 200) - bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300) """ + bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300) payload = { "mode": "command", @@ -657,7 +651,7 @@ class TestAgentViews(TacticalTestCase): # TODO mock the script - self.check_not_authenticated("post", url) + self.check_not_authenticated("post", url) """ @patch("agents.models.Agent.nats_cmd") def test_recover_mesh(self, nats_cmd): @@ -759,41 +753,6 @@ class TestAgentTasks(TacticalTestCase): self.authenticate() self.setup_coresettings() - @patch("agents.models.Agent.salt_api_cmd") - def test_sync_salt_modules_task(self, salt_api_cmd): - self.agent = baker.make_recipe("agents.agent") - salt_api_cmd.return_value = {"return": [{f"{self.agent.salt_id}": []}]} - ret = sync_salt_modules_task.s(self.agent.pk).apply() - salt_api_cmd.assert_called_with(timeout=35, func="saltutil.sync_modules") - self.assertEqual( - ret.result, f"Successfully synced salt modules on {self.agent.hostname}" - ) - self.assertEqual(ret.status, "SUCCESS") - - salt_api_cmd.return_value = "timeout" - ret = sync_salt_modules_task.s(self.agent.pk).apply() - self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}") - - salt_api_cmd.return_value = "error" - ret = sync_salt_modules_task.s(self.agent.pk).apply() - self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}") - - @patch("agents.models.Agent.salt_batch_async", return_value=None) - @patch("agents.tasks.sleep", return_value=None) - def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async): - # chunks of 50, should run 4 times - baker.make_recipe( - "agents.online_agent", last_seen=djangotime.now(), _quantity=60 - ) - baker.make_recipe( - "agents.overdue_agent", - last_seen=djangotime.now() - djangotime.timedelta(minutes=9), - _quantity=115, - ) - ret = batch_sync_modules_task.s().apply() - self.assertEqual(salt_batch_async.call_count, 4) - self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.nats_cmd") def test_agent_update(self, nats_cmd): from agents.tasks import agent_update diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 2f26b600..a69fcc8b 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -7,6 +7,7 @@ import random import string import datetime as dt from packaging import version as pyver +from typing import List from django.conf import settings from django.shortcuts import get_object_or_404 @@ -37,7 +38,7 @@ from .tasks import ( send_agent_update_task, run_script_email_results_task, ) -from winupdate.tasks import bulk_check_for_updates_task +from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task from tacticalrmm.utils import notify_error, reload_nats @@ -72,10 +73,6 @@ def ping(request, pk): r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5)) if r == "pong": status = "online" - else: - r = agent.salt_api_cmd(timeout=5, func="test.ping") - if isinstance(r, bool) and r: - status = "online" return Response({"name": agent.hostname, "status": status}) @@ -849,8 +846,7 @@ def bulk(request): elif request.data["monType"] == "workstations": q = q.filter(monitoring_type="workstation") - minions = [agent.salt_id for agent in q] - agents = [agent.pk for agent in q] + agents: List[int] = [agent.pk for agent in q] AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data) @@ -868,14 +864,12 @@ def bulk(request): return Response(f"{script.name} will now be run on {len(agents)} agents") elif request.data["mode"] == "install": - r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates") - if r == "timeout": - return notify_error("Salt API not running") + bulk_install_updates_task.delay(agents) return Response( f"Pending updates will now be installed on {len(agents)} agents" ) elif request.data["mode"] == "scan": - bulk_check_for_updates_task.delay(minions=minions) + bulk_check_for_updates_task.delay(agents) return Response(f"Patch status scan will now run on {len(agents)} agents") return notify_error("Something went wrong") diff --git a/api/tacticalrmm/apiv2/__init__.py b/api/tacticalrmm/apiv2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/tacticalrmm/apiv2/apps.py b/api/tacticalrmm/apiv2/apps.py deleted file mode 100644 index 2d003f35..00000000 --- a/api/tacticalrmm/apiv2/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class Apiv2Config(AppConfig): - name = "apiv2" diff --git a/api/tacticalrmm/apiv2/migrations/__init__.py b/api/tacticalrmm/apiv2/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/tacticalrmm/apiv2/tests.py b/api/tacticalrmm/apiv2/tests.py deleted file mode 100644 index 58387e85..00000000 --- a/api/tacticalrmm/apiv2/tests.py +++ /dev/null @@ -1,38 +0,0 @@ -from tacticalrmm.test import TacticalTestCase -from unittest.mock import patch -from model_bakery import baker -from itertools import cycle - - -class TestAPIv2(TacticalTestCase): - def setUp(self): - self.authenticate() - self.setup_coresettings() - - @patch("agents.models.Agent.salt_api_cmd") - def test_sync_modules(self, mock_ret): - # setup data - agent = baker.make_recipe("agents.agent") - url = "/api/v2/saltminion/" - payload = {"agent_id": agent.agent_id} - - mock_ret.return_value = "error" - r = self.client.patch(url, payload, format="json") - self.assertEqual(r.status_code, 400) - - mock_ret.return_value = [] - r = self.client.patch(url, payload, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, "Modules are already in sync") - - mock_ret.return_value = ["modules.win_agent"] - r = self.client.patch(url, payload, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, "Successfully synced salt modules") - - mock_ret.return_value = ["askdjaskdjasd", "modules.win_agent"] - r = self.client.patch(url, payload, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, "Successfully synced salt modules") - - self.check_not_authenticated("patch", url) diff --git a/api/tacticalrmm/apiv2/urls.py b/api/tacticalrmm/apiv2/urls.py deleted file mode 100644 index 30852e36..00000000 --- a/api/tacticalrmm/apiv2/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import path -from . import views -from apiv3 import views as v3_views - -urlpatterns = [ - path("newagent/", v3_views.NewAgent.as_view()), - path("meshexe/", v3_views.MeshExe.as_view()), - path("saltminion/", v3_views.SaltMinion.as_view()), - path("/saltminion/", v3_views.SaltMinion.as_view()), - path("sysinfo/", v3_views.SysInfo.as_view()), - path("hello/", v3_views.Hello.as_view()), - path("checkrunner/", views.CheckRunner.as_view()), - path("/checkrunner/", views.CheckRunner.as_view()), -] diff --git a/api/tacticalrmm/apiv2/views.py b/api/tacticalrmm/apiv2/views.py deleted file mode 100644 index d44ede38..00000000 --- a/api/tacticalrmm/apiv2/views.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django.utils import timezone as djangotime - -from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from agents.models import Agent -from checks.models import Check - -from checks.serializers import CheckRunnerGetSerializerV2 - - -class CheckRunner(APIView): - """ - For the windows python agent - """ - - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, agentid): - agent = get_object_or_404(Agent, agent_id=agentid) - agent.last_seen = djangotime.now() - agent.save(update_fields=["last_seen"]) - checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False) - - ret = { - "agent": agent.pk, - "check_interval": agent.check_interval, - "checks": CheckRunnerGetSerializerV2(checks, many=True).data, - } - return Response(ret) - - def patch(self, request): - check = get_object_or_404(Check, pk=request.data["id"]) - check.last_run = djangotime.now() - check.save(update_fields=["last_run"]) - status = check.handle_checkv2(request.data) - return Response(status) diff --git a/api/tacticalrmm/apiv3/tests.py b/api/tacticalrmm/apiv3/tests.py index 91f533fa..adb5e22a 100644 --- a/api/tacticalrmm/apiv3/tests.py +++ b/api/tacticalrmm/apiv3/tests.py @@ -26,23 +26,6 @@ class TestAPIv3(TacticalTestCase): self.check_not_authenticated("get", url) - def test_get_salt_minion(self): - url = f"/api/v3/{self.agent.agent_id}/saltminion/" - url2 = f"/api/v2/{self.agent.agent_id}/saltminion/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertIn("latestVer", r.json().keys()) - self.assertIn("currentVer", r.json().keys()) - self.assertIn("salt_id", r.json().keys()) - self.assertIn("downloadURL", r.json().keys()) - - r2 = self.client.get(url2) - self.assertEqual(r2.status_code, 200) - - self.check_not_authenticated("get", url) - self.check_not_authenticated("get", url2) - def test_get_mesh_info(self): url = f"/api/v3/{self.agent.pk}/meshinfo/" diff --git a/api/tacticalrmm/apiv3/urls.py b/api/tacticalrmm/apiv3/urls.py index 25464377..6c2a7415 100644 --- a/api/tacticalrmm/apiv3/urls.py +++ b/api/tacticalrmm/apiv3/urls.py @@ -7,8 +7,6 @@ urlpatterns = [ path("checkrunner/", views.CheckRunner.as_view()), path("/checkrunner/", views.CheckRunner.as_view()), path("//taskrunner/", views.TaskRunner.as_view()), - path("saltminion/", views.SaltMinion.as_view()), - path("/saltminion/", views.SaltMinion.as_view()), path("/meshinfo/", views.MeshInfo.as_view()), path("meshexe/", views.MeshExe.as_view()), path("sysinfo/", views.SysInfo.as_view()), diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 6702bc2a..cd94e2c0 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -29,10 +29,8 @@ from winupdate.serializers import ApprovedUpdateSerializer from agents.tasks import ( agent_recovery_email_task, agent_recovery_sms_task, - sync_salt_modules_task, install_salt_task, ) -from winupdate.tasks import check_for_updates_task from checks.utils import bytes2human from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList @@ -131,12 +129,6 @@ class CheckIn(APIView): serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save(last_seen=djangotime.now()) - - sync_salt_modules_task.delay(agent.pk) - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": True} - ) - return Response("ok") @@ -223,12 +215,6 @@ class Hello(APIView): serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save(last_seen=djangotime.now()) - - sync_salt_modules_task.delay(agent.pk) - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": True} - ) - return Response("ok") @@ -298,77 +284,6 @@ class TaskRunner(APIView): return Response("ok") -class SaltMinion(APIView): - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, agentid): - agent = get_object_or_404(Agent, agent_id=agentid) - ret = { - "latestVer": settings.LATEST_SALT_VER, - "currentVer": agent.salt_ver, - "salt_id": agent.salt_id, - "downloadURL": agent.winsalt_dl, - } - return Response(ret) - - def post(self, request): - # accept the salt key - agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) - if agent.salt_id != request.data["saltid"]: - return notify_error("Salt keys do not match") - - try: - resp = requests.post( - f"http://{settings.SALT_HOST}:8123/run", - json=[ - { - "client": "wheel", - "fun": "key.accept", - "match": request.data["saltid"], - "username": settings.SALT_USERNAME, - "password": settings.SALT_PASSWORD, - "eauth": "pam", - } - ], - timeout=30, - ) - except Exception: - return notify_error("No communication between agent and salt-api") - - try: - data = resp.json()["return"][0]["data"] - minion = data["return"]["minions"][0] - except Exception: - return notify_error("Key error") - - if data["success"] and minion == request.data["saltid"]: - return Response("Salt key was accepted") - else: - return notify_error("Not accepted") - - def patch(self, request): - # sync modules - agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) - r = agent.salt_api_cmd(timeout=45, func="saltutil.sync_modules") - - if r == "timeout" or r == "error": - return notify_error("Failed to sync salt modules") - - if isinstance(r, list) and any("modules" in i for i in r): - return Response("Successfully synced salt modules") - elif isinstance(r, list) and not r: - return Response("Modules are already in sync") - else: - return notify_error(f"Failed to sync salt modules: {str(r)}") - - def put(self, request): - agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) - agent.salt_ver = request.data["ver"] - agent.save(update_fields=["salt_ver"]) - return Response("ok") - - class WinUpdater(APIView): authentication_classes = [TokenAuthentication] @@ -430,19 +345,10 @@ class WinUpdater(APIView): if reboot: if agent.has_nats: asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) - else: - agent.salt_api_async( - func="system.reboot", - arg=7, - kwargs={"in_seconds": True}, + logger.info( + f"{agent.hostname} is rebooting after updates were installed." ) - logger.info(f"{agent.hostname} is rebooting after updates were installed.") - else: - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": False} - ) - return Response("ok") diff --git a/api/tacticalrmm/scripts/tasks.py b/api/tacticalrmm/scripts/tasks.py index 3f8ea182..7871b73e 100644 --- a/api/tacticalrmm/scripts/tasks.py +++ b/api/tacticalrmm/scripts/tasks.py @@ -6,60 +6,26 @@ from scripts.models import Script @app.task -def handle_bulk_command_task(agentpks, cmd, shell, timeout): +def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None: agents = Agent.objects.filter(pk__in=agentpks) - agents_nats = [agent for agent in agents if agent.has_nats] - agents_salt = [agent for agent in agents if not agent.has_nats] - minions = [agent.salt_id for agent in agents_salt] - - if minions: - Agent.salt_batch_async( - minions=minions, - func="cmd.run_bg", - kwargs={ - "cmd": cmd, - "shell": shell, - "timeout": timeout, - }, - ) - - if agents_nats: - nats_data = { - "func": "rawcmd", - "timeout": timeout, - "payload": { - "command": cmd, - "shell": shell, - }, - } - for agent in agents_nats: - asyncio.run(agent.nats_cmd(nats_data, wait=False)) + nats_data = { + "func": "rawcmd", + "timeout": timeout, + "payload": { + "command": cmd, + "shell": shell, + }, + } + for agent in agents_nats: + asyncio.run(agent.nats_cmd(nats_data, wait=False)) @app.task -def handle_bulk_script_task(scriptpk, agentpks, args, timeout): +def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None: script = Script.objects.get(pk=scriptpk) agents = Agent.objects.filter(pk__in=agentpks) - agents_nats = [agent for agent in agents if agent.has_nats] - agents_salt = [agent for agent in agents if not agent.has_nats] - minions = [agent.salt_id for agent in agents_salt] - - if minions: - Agent.salt_batch_async( - minions=minions, - func="win_agent.run_script", - kwargs={ - "filepath": script.filepath, - "filename": script.filename, - "shell": script.shell, - "timeout": timeout, - "args": args, - "bg": True if script.shell == "python" else False, # salt bg script bug - }, - ) - nats_data = { "func": "runscript", "timeout": timeout, diff --git a/api/tacticalrmm/tacticalrmm/middleware.py b/api/tacticalrmm/tacticalrmm/middleware.py index c93b638f..3aed4a48 100644 --- a/api/tacticalrmm/tacticalrmm/middleware.py +++ b/api/tacticalrmm/tacticalrmm/middleware.py @@ -16,7 +16,6 @@ def get_debug_info(): EXCLUDE_PATHS = ( "/natsapi", "/api/v3", - "/api/v2", "/logs/auditlogs", f"/{settings.ADMIN_URL}", "/logout", diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index c0f79c33..22f40fc3 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -58,7 +58,6 @@ INSTALLED_APPS = [ "knox", "corsheaders", "accounts", - "apiv2", "apiv3", "clients", "agents", diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 78d29455..8e608d1e 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -10,7 +10,6 @@ urlpatterns = [ path("login/", LoginView.as_view()), path("logout/", knox_views.LogoutView.as_view()), path("logoutall/", knox_views.LogoutAllView.as_view()), - path("api/v2/", include("apiv2.urls")), path("api/v3/", include("apiv3.urls")), path("clients/", include("clients.urls")), path("agents/", include("agents.urls")), diff --git a/api/tacticalrmm/winupdate/tasks.py b/api/tacticalrmm/winupdate/tasks.py index 4bda0074..76acad15 100644 --- a/api/tacticalrmm/winupdate/tasks.py +++ b/api/tacticalrmm/winupdate/tasks.py @@ -1,9 +1,12 @@ -from time import sleep +import asyncio +import time from django.utils import timezone as djangotime from django.conf import settings import datetime as dt import pytz from loguru import logger +from packaging import version as pyver +from typing import List from agents.models import Agent from .models import WinUpdate @@ -23,22 +26,31 @@ def auto_approve_updates_task(): except: continue - online = [i for i in agents if i.status == "online"] + online = [ + i + for i in agents + if i.status == "online" and pyver.parse(i.version) >= pyver.parse("1.3.0") + ] - for agent in online: - - # check for updates on agent - check_for_updates_task.apply_async( - queue="wupdate", - kwargs={"pk": agent.pk, "wait": False, "auto_approve": True}, - ) + chunks = (online[i : i + 40] for i in range(0, len(online), 40)) + for chunk in chunks: + for agent in chunk: + asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) + time.sleep(0.05) + time.sleep(15) @app.task def check_agent_update_schedule_task(): # scheduled task that installs updates on agents if enabled agents = Agent.objects.all() - online = [i for i in agents if i.has_patches_pending and i.status == "online"] + online = [ + i + for i in agents + if pyver.parse(i.version) >= pyver.parse("1.3.0") + and i.has_patches_pending + and i.status == "online" + ] for agent in online: install = False @@ -98,117 +110,38 @@ def check_agent_update_schedule_task(): if install: # initiate update on agent asynchronously and don't worry about ret code logger.info(f"Installing windows updates on {agent.salt_id}") - agent.salt_api_async(func="win_agent.install_updates") + nats_data = { + "func": "installwinupdates", + "guids": agent.get_approved_update_guids(), + } + asyncio.run(agent.nats_cmd(nats_data, wait=False)) agent.patches_last_installed = djangotime.now() agent.save(update_fields=["patches_last_installed"]) @app.task -def check_for_updates_task(pk, wait=False, auto_approve=False): - - if wait: - sleep(120) - - agent = Agent.objects.get(pk=pk) - ret = agent.salt_api_cmd( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - - if ret == "timeout" or ret == "error": - return - - if isinstance(ret, str): - err = ["unknown failure", "2147352567", "2145107934"] - if any(x in ret.lower() for x in err): - logger.warning(f"{agent.salt_id}: {ret}") - return "failed" - - guids = [] - try: - for k in ret.keys(): - guids.append(k) - except Exception as e: - logger.error(f"{agent.salt_id}: {str(e)}") - return - - for i in guids: - # check if existing update install / download status has changed - if WinUpdate.objects.filter(agent=agent).filter(guid=i).exists(): - - update = WinUpdate.objects.filter(agent=agent).get(guid=i) - - # salt will report an update as not installed even if it has been installed if a reboot is pending - # ignore salt's return if the result field is 'success' as that means the agent has successfully installed the update - if update.result != "success": - if ret[i]["Installed"] != update.installed: - update.installed = not update.installed - update.save(update_fields=["installed"]) - - if ret[i]["Downloaded"] != update.downloaded: - update.downloaded = not update.downloaded - update.save(update_fields=["downloaded"]) - - # otherwise it's a new update - else: - WinUpdate( - agent=agent, - guid=i, - kb=ret[i]["KBs"][0], - mandatory=ret[i]["Mandatory"], - title=ret[i]["Title"], - needs_reboot=ret[i]["NeedsReboot"], - installed=ret[i]["Installed"], - downloaded=ret[i]["Downloaded"], - description=ret[i]["Description"], - severity=ret[i]["Severity"], - ).save() - - agent.delete_superseded_updates() - - # win_wua.list doesn't always return everything - # use win_wua.installed to check for any updates that it missed - # and then change update status to match - installed = agent.salt_api_cmd( - timeout=60, func="win_wua.installed", arg="kbs_only=True" - ) - - if installed == "timeout" or installed == "error": - pass - elif isinstance(installed, list): - agent.winupdates.filter(kb__in=installed).filter(installed=False).update( - installed=True, downloaded=True - ) - - # check if reboot needed. returns bool - needs_reboot = agent.salt_api_cmd(timeout=30, func="win_wua.get_needs_reboot") - - if needs_reboot == "timeout" or needs_reboot == "error": - pass - elif isinstance(needs_reboot, bool) and needs_reboot: - agent.needs_reboot = True - agent.save(update_fields=["needs_reboot"]) - else: - agent.needs_reboot = False - agent.save(update_fields=["needs_reboot"]) - - # approve updates if specified - if auto_approve: - agent.approve_updates() - - return "ok" +def bulk_install_updates_task(pks: List[int]) -> None: + q = Agent.objects.filter(pk__in=pks) + agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] + chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) + for chunk in chunks: + for agent in chunk: + nats_data = { + "func": "installwinupdates", + "guids": agent.get_approved_update_guids(), + } + asyncio.run(agent.nats_cmd(nats_data, wait=False)) + time.sleep(0.05) + time.sleep(15) @app.task -def bulk_check_for_updates_task(minions): - # don't flood the celery queue - chunks = (minions[i : i + 30] for i in range(0, len(minions), 30)) +def bulk_check_for_updates_task(pks: List[int]) -> None: + q = Agent.objects.filter(pk__in=pks) + agents = [i for i in q if pyver.parse(i.version) >= pyver.parse("1.3.0")] + chunks = (agents[i : i + 40] for i in range(0, len(agents), 40)) for chunk in chunks: - for i in chunk: - agent = Agent.objects.get(salt_id=i) - check_for_updates_task.apply_async( - queue="wupdate", - kwargs={"pk": agent.pk, "wait": False, "auto_approve": True}, - ) - sleep(30) + for agent in chunk: + asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) + time.sleep(0.05) + time.sleep(15) diff --git a/api/tacticalrmm/winupdate/tests.py b/api/tacticalrmm/winupdate/tests.py index f235003e..4681c11c 100644 --- a/api/tacticalrmm/winupdate/tests.py +++ b/api/tacticalrmm/winupdate/tests.py @@ -29,7 +29,7 @@ class TestWinUpdateViews(TacticalTestCase): self.check_not_authenticated("get", url) - @patch("winupdate.tasks.check_for_updates_task.apply_async") + """ @patch("winupdate.tasks.check_for_updates_task.apply_async") def test_run_update_scan(self, mock_task): # test a call where agent doesn't exist @@ -46,9 +46,9 @@ class TestWinUpdateViews(TacticalTestCase): kwargs={"pk": agent.pk, "wait": False, "auto_approve": True}, ) - self.check_not_authenticated("get", url) + self.check_not_authenticated("get", url) """ - @patch("agents.models.Agent.salt_api_cmd") + """ @patch("agents.models.Agent.salt_api_cmd") def test_install_updates(self, mock_cmd): # test a call where agent doesn't exist @@ -84,7 +84,7 @@ class TestWinUpdateViews(TacticalTestCase): resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.check_not_authenticated("get", url) + self.check_not_authenticated("get", url) """ def test_edit_policy(self): url = "/winupdate/editpolicy/" @@ -144,7 +144,7 @@ class WinupdateTasks(TacticalTestCase): for update in winupdates: self.assertEqual(update.action, "approve") - @patch("agents.models.Agent.salt_api_async") + """ @patch("agents.models.Agent.salt_api_async") def test_check_agent_update_daily_schedule(self, agent_salt_cmd): from .tasks import check_agent_update_schedule_task @@ -173,7 +173,7 @@ class WinupdateTasks(TacticalTestCase): check_agent_update_schedule_task() agent_salt_cmd.assert_called_with(func="win_agent.install_updates") - self.assertEquals(agent_salt_cmd.call_count, 2) + self.assertEquals(agent_salt_cmd.call_count, 2) """ """ @patch("agents.models.Agent.salt_api_async") def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): @@ -204,110 +204,4 @@ class WinupdateTasks(TacticalTestCase): check_agent_update_schedule_task() agent_salt_cmd.assert_called_with(func="win_agent.install_updates") - self.assertEquals(agent_salt_cmd.call_count, 2) """ - - @patch("agents.models.Agent.salt_api_cmd") - def test_check_for_updates(self, salt_api_cmd): - from .tasks import check_for_updates_task - - # create a matching update returned from salt - baker.make_recipe( - "winupdate.approved_winupdate", - agent=self.online_agents[0], - kb="KB12341234", - guid="GUID1", - downloaded=True, - severity="", - installed=True, - ) - - salt_success_return = { - "GUID1": { - "Title": "Update Title", - "KBs": ["KB12341234"], - "GUID": "GUID1", - "Description": "Description", - "Downloaded": False, - "Installed": False, - "Mandatory": False, - "Severity": "", - "NeedsReboot": True, - }, - "GUID2": { - "Title": "Update Title 2", - "KBs": ["KB12341235"], - "GUID": "GUID2", - "Description": "Description", - "Downloaded": False, - "Installed": True, - "Mandatory": False, - "Severity": "", - "NeedsReboot": True, - }, - } - - salt_kb_list = ["KB12341235"] - - # mock failed attempt - salt_api_cmd.return_value = "timeout" - ret = check_for_updates_task(self.online_agents[0].pk) - salt_api_cmd.assert_called_with( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - self.assertFalse(ret) - salt_api_cmd.reset_mock() - - # mock failed attempt - salt_api_cmd.return_value = "error" - ret = check_for_updates_task(self.online_agents[0].pk) - salt_api_cmd.assert_called_with( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - self.assertFalse(ret) - salt_api_cmd.reset_mock() - - # mock failed attempt - salt_api_cmd.return_value = "unknown failure" - ret = check_for_updates_task(self.online_agents[0].pk) - salt_api_cmd.assert_called_with( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - self.assertEquals(ret, "failed") - salt_api_cmd.reset_mock() - - # mock failed attempt at salt list updates with reboot - salt_api_cmd.side_effect = [salt_success_return, "timeout", True] - ret = check_for_updates_task(self.online_agents[0].pk) - salt_api_cmd.assert_any_call( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - salt_api_cmd.assert_any_call( - timeout=60, func="win_wua.installed", arg="kbs_only=True" - ) - - salt_api_cmd.assert_any_call(timeout=30, func="win_wua.get_needs_reboot") - - salt_api_cmd.reset_mock() - - # mock successful attempt without reboot - salt_api_cmd.side_effect = [salt_success_return, salt_kb_list, False] - ret = check_for_updates_task(self.online_agents[0].pk) - salt_api_cmd.assert_any_call( - timeout=310, - func="win_wua.list", - arg="skip_installed=False", - ) - - salt_api_cmd.assert_any_call( - timeout=60, func="win_wua.installed", arg="kbs_only=True" - ) - - salt_api_cmd.assert_any_call(timeout=30, func="win_wua.get_needs_reboot") + self.assertEquals(agent_salt_cmd.call_count, 2) """ \ No newline at end of file diff --git a/api/tacticalrmm/winupdate/views.py b/api/tacticalrmm/winupdate/views.py index 52beaf77..67575ace 100644 --- a/api/tacticalrmm/winupdate/views.py +++ b/api/tacticalrmm/winupdate/views.py @@ -1,10 +1,8 @@ +import asyncio +from packaging import version as pyver from django.shortcuts import get_object_or_404 -from rest_framework.decorators import ( - api_view, - authentication_classes, - permission_classes, -) +from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated @@ -12,7 +10,6 @@ from rest_framework.permissions import IsAuthenticated from agents.models import Agent from .models import WinUpdate from .serializers import UpdateSerializer, ApprovedUpdateSerializer -from .tasks import check_for_updates_task from tacticalrmm.utils import notify_error @@ -25,30 +22,24 @@ def get_win_updates(request, pk): @api_view() def run_update_scan(request, pk): agent = get_object_or_404(Agent, pk=pk) - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": False, "auto_approve": True} - ) + if pyver.parse(agent.version) < pyver.parse("1.3.0"): + return notify_error("Requires agent version 1.3.0 or greater") + + asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) return Response("ok") @api_view() def install_updates(request, pk): agent = get_object_or_404(Agent, pk=pk) - r = agent.salt_api_cmd(timeout=15, func="win_agent.install_updates") - - if r == "timeout": - return notify_error("Unable to contact the agent") - elif r == "error": - return notify_error("Something went wrong") - elif r == "running": - return notify_error(f"Updates are already being installed on {agent.hostname}") - - # successful response: {'return': [{'SALT-ID': {'pid': 3316}}]} - try: - r["pid"] - except (KeyError): - return notify_error(str(r)) + if pyver.parse(agent.version) < pyver.parse("1.3.0"): + return notify_error("Requires agent version 1.3.0 or greater") + nats_data = { + "func": "installwinupdates", + "guids": agent.get_approved_update_guids(), + } + asyncio.run(agent.nats_cmd(nats_data, wait=False)) return Response(f"Patches will now be installed on {agent.hostname}") diff --git a/backup.sh b/backup.sh index 85722a88..0a316493 100755 --- a/backup.sh +++ b/backup.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="6" +SCRIPT_VERSION="7" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' GREEN='\033[0;32m' @@ -61,7 +61,6 @@ sysd="/etc/systemd/system" mkdir -p ${tmp_dir}/meshcentral/mongo mkdir ${tmp_dir}/postgres -mkdir ${tmp_dir}/salt mkdir ${tmp_dir}/certs mkdir ${tmp_dir}/nginx mkdir ${tmp_dir}/systemd @@ -74,16 +73,13 @@ pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432 tar -czvf ${tmp_dir}/meshcentral/mesh.tar.gz --exclude=/meshcentral/node_modules /meshcentral mongodump --gzip --out=${tmp_dir}/meshcentral/mongo -sudo tar -czvf ${tmp_dir}/salt/etc-salt.tar.gz -C /etc/salt . -tar -czvf ${tmp_dir}/salt/srv-salt.tar.gz -C /srv/salt . - sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx . sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d . -sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/celery-winupdate.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/natsapi.service ${tmp_dir}/systemd/ +sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/natsapi.service ${tmp_dir}/systemd/ cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/ diff --git a/docker/readme.md b/docker/readme.md index 8ee987fd..a51ecba7 100644 --- a/docker/readme.md +++ b/docker/readme.md @@ -18,7 +18,7 @@ sudo certbot certonly --manual -d *.example.com --agree-tos --no-bootstrap --man ## Configure DNS and firewall -You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443, 4222, 4505, 4506. +You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443 and 4222. ## Setting up the environment diff --git a/install.sh b/install.sh index 47968690..5759a865 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="33" +SCRIPT_VERSION="34" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh' sudo apt install -y curl wget @@ -184,11 +184,6 @@ CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem sudo chown ${USER}:${USER} -R /etc/letsencrypt sudo chmod 775 -R /etc/letsencrypt -print_green 'Creating saltapi user' - -sudo adduser --no-create-home --disabled-password --gecos "" saltapi -echo "saltapi:${SALTPW}" | sudo chpasswd - print_green 'Installing golang' sudo mkdir -p /usr/local/rmmgo @@ -239,7 +234,7 @@ sudo systemctl restart mongod print_green 'Installing python, redis and git' sudo apt update -sudo apt install -y python3-venv python3-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git +sudo apt install -y python3-venv python3-dev python3-pip python3-setuptools python3-wheel ca-certificates redis git print_green 'Installing postgresql' @@ -359,9 +354,6 @@ if not DEBUG: ) }) -SALT_USERNAME = "saltapi" -SALT_PASSWORD = "${SALTPW}" -SALT_HOST = "127.0.0.1" MESH_USERNAME = "${meshusername}" MESH_SITE = "https://${meshdomain}" REDIS_HOST = "localhost" @@ -602,46 +594,6 @@ echo "${nginxmesh}" | sudo tee /etc/nginx/sites-available/meshcentral.conf > /de sudo ln -s /etc/nginx/sites-available/rmm.conf /etc/nginx/sites-enabled/rmm.conf sudo ln -s /etc/nginx/sites-available/meshcentral.conf /etc/nginx/sites-enabled/meshcentral.conf -print_green 'Installing Salt Master' -wget -O - 'https://repo.saltstack.com/py3/'$osname'/'$fullrelno'/amd64/latest/SALTSTACK-GPG-KEY.pub' | sudo apt-key add - -echo 'deb http://repo.saltstack.com/py3/'$osname'/'$fullrelno'/amd64/latest '$codename' main' | sudo tee /etc/apt/sources.list.d/saltstack.list - -sudo apt update -sudo apt install -y salt-master - -print_green 'Waiting 10 seconds for salt to start' -sleep 10 - -saltvars="$(cat << EOF -timeout: 20 -gather_job_timeout: 25 -max_event_size: 30485760 -external_auth: - pam: - saltapi: - - .* - - '@runner' - - '@wheel' - - '@jobs' - -rest_cherrypy: - port: 8123 - disable_ssl: True - max_request_body_size: 30485760 - -EOF -)" -echo "${saltvars}" | sudo tee /etc/salt/master.d/rmm-salt.conf > /dev/null - -# fix the stupid 1 MB limit present in msgpack 0.6.2, which btw was later changed to 100 MB in msgpack 1.0.0 -# but 0.6.2 is the default on ubuntu 20 -sudo sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py - - - -print_green 'Installing Salt API' -sudo apt install -y salt-api - sudo mkdir /etc/conf.d celeryservice="$(cat << EOF @@ -676,7 +628,7 @@ CELERY_APP="tacticalrmm" CELERYD_MULTI="multi" -CELERYD_OPTS="--time-limit=2900 --autoscale=50,5" +CELERYD_OPTS="--time-limit=9999 --autoscale=100,5" CELERYD_PID_FILE="/rmm/api/tacticalrmm/%n.pid" CELERYD_LOG_FILE="/var/log/celery/%n%I.log" @@ -688,44 +640,6 @@ EOF )" echo "${celeryconf}" | sudo tee /etc/conf.d/celery.conf > /dev/null -celerywinupdatesvc="$(cat << EOF -[Unit] -Description=Celery WinUpdate Service V2 -After=network.target redis-server.service postgresql.service - -[Service] -Type=forking -User=${USER} -Group=${USER} -EnvironmentFile=/etc/conf.d/celery-winupdate.conf -WorkingDirectory=/rmm/api/tacticalrmm -ExecStart=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi start \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" -Q wupdate \$CELERYD_OPTS' -ExecStop=/bin/sh -c '\${CELERY_BIN} multi stopwait \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --loglevel="\${CELERYD_LOG_LEVEL}"' -ExecReload=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi restart \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" -Q wupdate \$CELERYD_OPTS' -Restart=always -RestartSec=10s - -[Install] -WantedBy=multi-user.target -EOF -)" -echo "${celerywinupdatesvc}" | sudo tee /etc/systemd/system/celery-winupdate.service > /dev/null - -celerywinupdate="$(cat << EOF -CELERYD_NODES="w2" - -CELERY_BIN="/rmm/api/env/bin/celery" -CELERY_APP="tacticalrmm" -CELERYD_MULTI="multi" - -CELERYD_OPTS="--time-limit=4000 --autoscale=40,1" - -CELERYD_PID_FILE="/rmm/api/tacticalrmm/%n.pid" -CELERYD_LOG_FILE="/var/log/celery/%n%I.log" -CELERYD_LOG_LEVEL="ERROR" -EOF -)" -echo "${celerywinupdate}" | sudo tee /etc/conf.d/celery-winupdate.conf > /dev/null celerybeatservice="$(cat << EOF [Unit] @@ -748,21 +662,12 @@ EOF )" echo "${celerybeatservice}" | sudo tee /etc/systemd/system/celerybeat.service > /dev/null -sudo mkdir -p /srv/salt -sudo cp -r /rmm/_modules /srv/salt/ -sudo cp -r /rmm/scripts /srv/salt/ -sudo mkdir /srv/salt/scripts/userdefined -sudo chown ${USER}:${USER} -R /srv/salt/ -sudo chown ${USER}:www-data /srv/salt/scripts/userdefined -sudo chmod 750 /srv/salt/scripts/userdefined sudo chown ${USER}:${USER} -R /etc/conf.d/ meshservice="$(cat << EOF [Unit] Description=MeshCentral Server -After=network.target -After=mongod.service -After=nginx.service +After=network.target mongod.service nginx.service [Service] Type=simple LimitNOFILE=1000000 @@ -782,12 +687,6 @@ echo "${meshservice}" | sudo tee /etc/systemd/system/meshcentral.service > /dev/ sudo systemctl daemon-reload - -sudo systemctl enable salt-master -sudo systemctl enable salt-api - -sudo systemctl restart salt-api - sudo chown -R $USER:$GROUP /home/${USER}/.npm sudo chown -R $USER:$GROUP /home/${USER}/.config @@ -844,7 +743,7 @@ sudo ln -s /etc/nginx/sites-available/frontend.conf /etc/nginx/sites-enabled/fro print_green 'Enabling Services' -for i in rmm.service celery.service celerybeat.service celery-winupdate.service nginx +for i in rmm.service celery.service celerybeat.service nginx do sudo systemctl enable ${i} sudo systemctl stop ${i} @@ -912,17 +811,12 @@ sudo systemctl start nats.service print_green 'Restarting services' -for i in rmm.service celery.service celerybeat.service celery-winupdate.service natsapi.service +for i in rmm.service celery.service celerybeat.service natsapi.service do sudo systemctl stop ${i} sudo systemctl start ${i} done -print_green 'Restarting salt-master and waiting 10 seconds' -sudo systemctl restart salt-master -sleep 10 -sudo systemctl restart salt-api - printf >&2 "${YELLOW}%0.s*${NC}" {1..80} printf >&2 "\n\n" printf >&2 "${YELLOW}Installation complete!${NC}\n\n" @@ -938,7 +832,7 @@ if [ "$BEHIND_NAT" = true ]; then echo -ne "${GREEN}If you will be accessing the web interface of the RMM from the same LAN as this server,${NC}\n" echo -ne "${GREEN}you'll need to make sure your 3 subdomains resolve to ${IPV4}${NC}\n" echo -ne "${GREEN}This also applies to any agents that will be on the same local network as the rmm.${NC}\n" - echo -ne "${GREEN}You'll also need to setup port forwarding in your router on ports 80, 443, 4505, 4506 and 4222 tcp.${NC}\n\n" + echo -ne "${GREEN}You'll also need to setup port forwarding in your router on ports 80, 443 and 4222 tcp.${NC}\n\n" fi printf >&2 "${YELLOW}Please refer to the github README for next steps${NC}\n\n" diff --git a/restore.sh b/restore.sh index d7a71ba5..00233628 100755 --- a/restore.sh +++ b/restore.sh @@ -7,7 +7,7 @@ pgpw="hunter2" ##################################################### -SCRIPT_VERSION="12" +SCRIPT_VERSION="13" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh' sudo apt install -y curl wget @@ -166,15 +166,9 @@ print_green 'Restoring systemd services' sudo cp $tmp_dir/systemd/* /etc/systemd/system/ sudo systemctl daemon-reload -print_green 'Restoring saltapi user' - -SALTPW=$(grep SALT_PASSWORD $tmp_dir/rmm/local_settings.py | tr -d " \t" | sed 's/.*=//' | tr -d '"') -sudo adduser --no-create-home --disabled-password --gecos "" saltapi -echo "saltapi:${SALTPW}" | sudo chpasswd - print_green 'Installing python, redis and git' -sudo apt install -y python3.8-venv python3.8-dev python3-pip python3-cherrypy3 python3-setuptools python3-wheel ca-certificates redis git +sudo apt install -y python3-venv python3-dev python3-pip python3-setuptools python3-wheel ca-certificates redis git print_green 'Installing postgresql' @@ -261,40 +255,6 @@ deactivate sudo systemctl enable nats.service sudo systemctl start nats.service -print_green 'Installing Salt Master' - -wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | sudo apt-key add - -echo 'deb http://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest focal main' | sudo tee /etc/apt/sources.list.d/saltstack.list - -sudo apt update -sudo apt install -y salt-master - -print_green 'Waiting 10 seconds for salt to start' -sleep 10 - -print_green 'Installing Salt API' -sudo apt install -y salt-api - -sudo sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py - -sudo systemctl enable salt-master -sudo systemctl enable salt-api -sudo systemctl restart salt-api -sleep 3 - -print_green 'Restoring salt keys' - -sudo systemctl stop salt-master -sudo systemctl stop salt-api -sudo rm -rf /etc/salt -sudo mkdir /etc/salt -sudo tar -xzf $tmp_dir/salt/etc-salt.tar.gz -C /etc/salt -sudo mkdir -p /srv/salt -sudo tar -xzf $tmp_dir/salt/srv-salt.tar.gz -C /srv/salt -sudo chown ${USER}:${USER} -R /srv/salt/ -sudo chown ${USER}:www-data /srv/salt/scripts/userdefined -sudo chmod 750 /srv/salt/scripts/userdefined - print_green 'Restoring the frontend' sudo chown -R $USER:$GROUP /home/${USER}/.npm @@ -310,18 +270,15 @@ sudo chown www-data:www-data -R /var/www/rmm/dist # reset perms sudo chown ${USER}:${USER} -R /rmm sudo chown ${USER}:${USER} /var/log/celery -sudo chown ${USER}:${USER} -R /srv/salt/ sudo chown ${USER}:${USER} -R /etc/conf.d/ -sudo chown ${USER}:www-data /srv/salt/scripts/userdefined sudo chown -R $USER:$GROUP /home/${USER}/.npm sudo chown -R $USER:$GROUP /home/${USER}/.config sudo chown -R $USER:$GROUP /home/${USER}/.cache -sudo chmod 750 /srv/salt/scripts/userdefined print_green 'Enabling Services' sudo systemctl daemon-reload -for i in celery.service celerybeat.service celery-winupdate.service rmm.service nginx +for i in celery.service celerybeat.service rmm.service nginx do sudo systemctl enable ${i} sudo systemctl stop ${i} @@ -337,11 +294,6 @@ print_green 'Starting natsapi' sudo systemctl enable natsapi.service sudo systemctl start natsapi.service -print_green 'Restarting salt and waiting 10 seconds' -sudo systemctl restart salt-master -sleep 10 -sudo systemctl restart salt-api - printf >&2 "${YELLOW}%0.s*${NC}" {1..80} printf >&2 "\n\n" printf >&2 "${YELLOW}Restore complete!${NC}\n\n" diff --git a/update.sh b/update.sh index 61bb3ba8..789ca0c8 100644 --- a/update.sh +++ b/update.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="103" +SCRIPT_VERSION="104" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh' LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py' YELLOW='\033[1;33m' @@ -107,41 +107,6 @@ sudo systemctl enable celerybeat.service fi -CHECK_CELERYWINUPDATE_V2=$(grep V2 /etc/systemd/system/celery-winupdate.service) -if ! [[ $CHECK_CELERYWINUPDATE_V2 ]]; then -printf >&2 "${GREEN}Updating celery-winupdate.service${NC}\n" -sudo systemctl stop celery-winupdate.service -sudo rm -f /etc/systemd/system/celery-winupdate.service - -celerywinupdatesvc="$(cat << EOF -[Unit] -Description=Celery WinUpdate Service V2 -After=network.target redis-server.service postgresql.service - -[Service] -Type=forking -User=${USER} -Group=${USER} -EnvironmentFile=/etc/conf.d/celery-winupdate.conf -WorkingDirectory=/rmm/api/tacticalrmm -ExecStart=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi start \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" -Q wupdate \$CELERYD_OPTS' -ExecStop=/bin/sh -c '\${CELERY_BIN} multi stopwait \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --loglevel="\${CELERYD_LOG_LEVEL}"' -ExecReload=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi restart \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" -Q wupdate \$CELERYD_OPTS' -Restart=always -RestartSec=10s - -[Install] -WantedBy=multi-user.target -EOF -)" -echo "${celerywinupdatesvc}" | sudo tee /etc/systemd/system/celery-winupdate.service > /dev/null - -sudo systemctl daemon-reload -sudo systemctl enable celery-winupdate.service - -fi - - TMP_SETTINGS=$(mktemp -p "" "rmmsettings_XXXXXXXXXX") curl -s -L "${LATEST_SETTINGS_URL}" > ${TMP_SETTINGS} SETTINGS_FILE="/rmm/api/tacticalrmm/tacticalrmm/settings.py" @@ -158,7 +123,6 @@ fi LATEST_MESH_VER=$(grep "^MESH_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') LATEST_PIP_VER=$(grep "^PIP_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') LATEST_NPM_VER=$(grep "^NPM_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') -LATEST_SALT_VER=$(grep "^SALT_MASTER_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') CURRENT_PIP_VER=$(grep "^PIP_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') CURRENT_NPM_VER=$(grep "^NPM_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') @@ -187,7 +151,15 @@ sudo systemctl daemon-reload sudo systemctl enable natsapi.service fi -for i in salt-master salt-api nginx nats natsapi rmm celery celerybeat celery-winupdate +if [ -f /etc/systemd/system/celery-winupdate.service ]; then + printf >&2 "${GREEN}Removing celery-winupdate.service${NC}\n" + sudo systemctl stop celery-winupdate.service + sudo systemctl disable celery-winupdate.service + sudo rm -f /etc/systemd/system/celery-winupdate.service + sudo systemctl daemon-reload +fi + +for i in nginx nats natsapi rmm celery celerybeat do printf >&2 "${GREEN}Stopping ${i} service...${NC}\n" sudo systemctl stop ${i} @@ -208,38 +180,16 @@ git reset --hard FETCH_HEAD git clean -df git pull -CHECK_SALT=$(sudo salt --version | grep ${LATEST_SALT_VER}) -if ! [[ $CHECK_SALT ]]; then - printf >&2 "${GREEN}Updating salt${NC}\n" - sudo apt update - sudo apt install -y salt-master salt-api salt-common - printf >&2 "${GREEN}Waiting for salt...${NC}\n" - sleep 15 - sudo systemctl stop salt-master - sudo systemctl stop salt-api - printf >&2 "${GREEN}Fixing msgpack${NC}\n" - sudo sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py - sudo systemctl start salt-master - printf >&2 "${GREEN}Waiting for salt...${NC}\n" - sleep 15 - sudo systemctl start salt-api - printf >&2 "${GREEN}Salt update finished${NC}\n" -fi sudo chown ${USER}:${USER} -R /rmm sudo chown ${USER}:${USER} /var/log/celery -sudo chown ${USER}:${USER} -R /srv/salt/ sudo chown ${USER}:${USER} -R /etc/conf.d/ -sudo chown ${USER}:www-data /srv/salt/scripts/userdefined sudo chown -R $USER:$GROUP /home/${USER}/.npm sudo chown -R $USER:$GROUP /home/${USER}/.config sudo chown -R $USER:$GROUP /home/${USER}/.cache -sudo chmod 750 /srv/salt/scripts/userdefined sudo chown ${USER}:${USER} -R /etc/letsencrypt sudo chmod 775 -R /etc/letsencrypt -cp /rmm/_modules/* /srv/salt/_modules/ -cp /rmm/scripts/* /srv/salt/scripts/ /usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/ sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo @@ -272,7 +222,6 @@ fi python manage.py pre_update_tasks python manage.py migrate python manage.py delete_tokens -python manage.py fix_salt_key python manage.py collectstatic --no-input python manage.py reload_nats python manage.py load_chocos @@ -292,13 +241,7 @@ sudo rm -rf /var/www/rmm/dist sudo cp -pr /rmm/web/dist /var/www/rmm/ sudo chown www-data:www-data -R /var/www/rmm/dist -printf >&2 "${GREEN}Starting salt-master service${NC}\n" -sudo systemctl start salt-master -sleep 7 -printf >&2 "${GREEN}Starting salt-api service${NC}\n" -sudo systemctl start salt-api - -for i in rmm celery celerybeat celery-winupdate nginx nats natsapi +for i in rmm celery celerybeat nginx nats natsapi do printf >&2 "${GREEN}Starting ${i} service${NC}\n" sudo systemctl start ${i}