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