goodbye salt, you've served us well

This commit is contained in:
wh1te909 2021-01-20 22:11:54 +00:00
parent 3282fa803c
commit 903af0c2cf
26 changed files with 125 additions and 930 deletions

View File

@ -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
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class Apiv2Config(AppConfig):
name = "apiv2"

View File

@ -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)

View File

@ -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("<str:agentid>/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("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
]

View File

@ -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)

View File

@ -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/"

View File

@ -7,8 +7,6 @@ urlpatterns = [
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("saltminion/", views.SaltMinion.as_view()),
path("<str:agentid>/saltminion/", views.SaltMinion.as_view()),
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
path("meshexe/", views.MeshExe.as_view()),
path("sysinfo/", views.SysInfo.as_view()),

View File

@ -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")

View File

@ -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,

View File

@ -16,7 +16,6 @@ def get_debug_info():
EXCLUDE_PATHS = (
"/natsapi",
"/api/v3",
"/api/v2",
"/logs/auditlogs",
f"/{settings.ADMIN_URL}",
"/logout",

View File

@ -58,7 +58,6 @@ INSTALLED_APPS = [
"knox",
"corsheaders",
"accounts",
"apiv2",
"apiv3",
"clients",
"agents",

View File

@ -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")),

View File

@ -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)

View File

@ -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) """

View File

@ -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}")

View File

@ -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/

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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}